diff --git a/libs/appdirs.py b/libs/appdirs.py new file mode 100644 index 00000000..ae67001a --- /dev/null +++ b/libs/appdirs.py @@ -0,0 +1,608 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (c) 2005-2010 ActiveState Software Inc. +# Copyright (c) 2013 Eddy Petrișor + +"""Utilities for determining application-specific dirs. + +See for details and usage. +""" +# Dev Notes: +# - MSDN on where to store app data files: +# http://support.microsoft.com/default.aspx?scid=kb;en-us;310294#XSLTH3194121123120121120120 +# - Mac OS X: http://developer.apple.com/documentation/MacOSX/Conceptual/BPFileSystem/index.html +# - XDG spec for Un*x: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html + +__version_info__ = (1, 4, 3) +__version__ = '.'.join(map(str, __version_info__)) + + +import sys +import os + +PY3 = sys.version_info[0] == 3 + +if PY3: + unicode = str + +if sys.platform.startswith('java'): + import platform + os_name = platform.java_ver()[3][0] + if os_name.startswith('Windows'): # "Windows XP", "Windows 7", etc. + system = 'win32' + elif os_name.startswith('Mac'): # "Mac OS X", etc. + system = 'darwin' + else: # "Linux", "SunOS", "FreeBSD", etc. + # Setting this to "linux2" is not ideal, but only Windows or Mac + # are actually checked for and the rest of the module expects + # *sys.platform* style strings. + system = 'linux2' +else: + system = sys.platform + + + +def user_data_dir(appname=None, appauthor=None, version=None, roaming=False): + r"""Return full path to the user-specific data dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "roaming" (boolean, default False) can be set True to use the Windows + roaming appdata directory. That means that for users on a Windows + network setup for roaming profiles, this user data will be + sync'd on login. See + + for a discussion of issues. + + Typical user data directories are: + Mac OS X: ~/Library/Application Support/ + Unix: ~/.local/share/ # or in $XDG_DATA_HOME, if defined + Win XP (not roaming): C:\Documents and Settings\\Application Data\\ + Win XP (roaming): C:\Documents and Settings\\Local Settings\Application Data\\ + Win 7 (not roaming): C:\Users\\AppData\Local\\ + Win 7 (roaming): C:\Users\\AppData\Roaming\\ + + For Unix, we follow the XDG spec and support $XDG_DATA_HOME. + That means, by default "~/.local/share/". + """ + if system == "win32": + if appauthor is None: + appauthor = appname + const = roaming and "CSIDL_APPDATA" or "CSIDL_LOCAL_APPDATA" + path = os.path.normpath(_get_win_folder(const)) + if appname: + if appauthor is not False: + path = os.path.join(path, appauthor, appname) + else: + path = os.path.join(path, appname) + elif system == 'darwin': + path = os.path.expanduser('~/Library/Application Support/') + if appname: + path = os.path.join(path, appname) + else: + path = os.getenv('XDG_DATA_HOME', os.path.expanduser("~/.local/share")) + if appname: + path = os.path.join(path, appname) + if appname and version: + path = os.path.join(path, version) + return path + + +def site_data_dir(appname=None, appauthor=None, version=None, multipath=False): + r"""Return full path to the user-shared data dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "multipath" is an optional parameter only applicable to *nix + which indicates that the entire list of data dirs should be + returned. By default, the first item from XDG_DATA_DIRS is + returned, or '/usr/local/share/', + if XDG_DATA_DIRS is not set + + Typical site data directories are: + Mac OS X: /Library/Application Support/ + Unix: /usr/local/share/ or /usr/share/ + Win XP: C:\Documents and Settings\All Users\Application Data\\ + Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.) + Win 7: C:\ProgramData\\ # Hidden, but writeable on Win 7. + + For Unix, this is using the $XDG_DATA_DIRS[0] default. + + WARNING: Do not use this on Windows. See the Vista-Fail note above for why. + """ + if system == "win32": + if appauthor is None: + appauthor = appname + path = os.path.normpath(_get_win_folder("CSIDL_COMMON_APPDATA")) + if appname: + if appauthor is not False: + path = os.path.join(path, appauthor, appname) + else: + path = os.path.join(path, appname) + elif system == 'darwin': + path = os.path.expanduser('/Library/Application Support') + if appname: + path = os.path.join(path, appname) + else: + # XDG default for $XDG_DATA_DIRS + # only first, if multipath is False + path = os.getenv('XDG_DATA_DIRS', + os.pathsep.join(['/usr/local/share', '/usr/share'])) + pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)] + if appname: + if version: + appname = os.path.join(appname, version) + pathlist = [os.sep.join([x, appname]) for x in pathlist] + + if multipath: + path = os.pathsep.join(pathlist) + else: + path = pathlist[0] + return path + + if appname and version: + path = os.path.join(path, version) + return path + + +def user_config_dir(appname=None, appauthor=None, version=None, roaming=False): + r"""Return full path to the user-specific config dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "roaming" (boolean, default False) can be set True to use the Windows + roaming appdata directory. That means that for users on a Windows + network setup for roaming profiles, this user data will be + sync'd on login. See + + for a discussion of issues. + + Typical user config directories are: + Mac OS X: same as user_data_dir + Unix: ~/.config/ # or in $XDG_CONFIG_HOME, if defined + Win *: same as user_data_dir + + For Unix, we follow the XDG spec and support $XDG_CONFIG_HOME. + That means, by default "~/.config/". + """ + if system in ["win32", "darwin"]: + path = user_data_dir(appname, appauthor, None, roaming) + else: + path = os.getenv('XDG_CONFIG_HOME', os.path.expanduser("~/.config")) + if appname: + path = os.path.join(path, appname) + if appname and version: + path = os.path.join(path, version) + return path + + +def site_config_dir(appname=None, appauthor=None, version=None, multipath=False): + r"""Return full path to the user-shared data dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "multipath" is an optional parameter only applicable to *nix + which indicates that the entire list of config dirs should be + returned. By default, the first item from XDG_CONFIG_DIRS is + returned, or '/etc/xdg/', if XDG_CONFIG_DIRS is not set + + Typical site config directories are: + Mac OS X: same as site_data_dir + Unix: /etc/xdg/ or $XDG_CONFIG_DIRS[i]/ for each value in + $XDG_CONFIG_DIRS + Win *: same as site_data_dir + Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.) + + For Unix, this is using the $XDG_CONFIG_DIRS[0] default, if multipath=False + + WARNING: Do not use this on Windows. See the Vista-Fail note above for why. + """ + if system in ["win32", "darwin"]: + path = site_data_dir(appname, appauthor) + if appname and version: + path = os.path.join(path, version) + else: + # XDG default for $XDG_CONFIG_DIRS + # only first, if multipath is False + path = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg') + pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)] + if appname: + if version: + appname = os.path.join(appname, version) + pathlist = [os.sep.join([x, appname]) for x in pathlist] + + if multipath: + path = os.pathsep.join(pathlist) + else: + path = pathlist[0] + return path + + +def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True): + r"""Return full path to the user-specific cache dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "opinion" (boolean) can be False to disable the appending of + "Cache" to the base app data dir for Windows. See + discussion below. + + Typical user cache directories are: + Mac OS X: ~/Library/Caches/ + Unix: ~/.cache/ (XDG default) + Win XP: C:\Documents and Settings\\Local Settings\Application Data\\\Cache + Vista: C:\Users\\AppData\Local\\\Cache + + On Windows the only suggestion in the MSDN docs is that local settings go in + the `CSIDL_LOCAL_APPDATA` directory. This is identical to the non-roaming + app data dir (the default returned by `user_data_dir` above). Apps typically + put cache data somewhere *under* the given dir here. Some examples: + ...\Mozilla\Firefox\Profiles\\Cache + ...\Acme\SuperApp\Cache\1.0 + OPINION: This function appends "Cache" to the `CSIDL_LOCAL_APPDATA` value. + This can be disabled with the `opinion=False` option. + """ + if system == "win32": + if appauthor is None: + appauthor = appname + path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA")) + if appname: + if appauthor is not False: + path = os.path.join(path, appauthor, appname) + else: + path = os.path.join(path, appname) + if opinion: + path = os.path.join(path, "Cache") + elif system == 'darwin': + path = os.path.expanduser('~/Library/Caches') + if appname: + path = os.path.join(path, appname) + else: + path = os.getenv('XDG_CACHE_HOME', os.path.expanduser('~/.cache')) + if appname: + path = os.path.join(path, appname) + if appname and version: + path = os.path.join(path, version) + return path + + +def user_state_dir(appname=None, appauthor=None, version=None, roaming=False): + r"""Return full path to the user-specific state dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "roaming" (boolean, default False) can be set True to use the Windows + roaming appdata directory. That means that for users on a Windows + network setup for roaming profiles, this user data will be + sync'd on login. See + + for a discussion of issues. + + Typical user state directories are: + Mac OS X: same as user_data_dir + Unix: ~/.local/state/ # or in $XDG_STATE_HOME, if defined + Win *: same as user_data_dir + + For Unix, we follow this Debian proposal + to extend the XDG spec and support $XDG_STATE_HOME. + + That means, by default "~/.local/state/". + """ + if system in ["win32", "darwin"]: + path = user_data_dir(appname, appauthor, None, roaming) + else: + path = os.getenv('XDG_STATE_HOME', os.path.expanduser("~/.local/state")) + if appname: + path = os.path.join(path, appname) + if appname and version: + path = os.path.join(path, version) + return path + + +def user_log_dir(appname=None, appauthor=None, version=None, opinion=True): + r"""Return full path to the user-specific log dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "opinion" (boolean) can be False to disable the appending of + "Logs" to the base app data dir for Windows, and "log" to the + base cache dir for Unix. See discussion below. + + Typical user log directories are: + Mac OS X: ~/Library/Logs/ + Unix: ~/.cache//log # or under $XDG_CACHE_HOME if defined + Win XP: C:\Documents and Settings\\Local Settings\Application Data\\\Logs + Vista: C:\Users\\AppData\Local\\\Logs + + On Windows the only suggestion in the MSDN docs is that local settings + go in the `CSIDL_LOCAL_APPDATA` directory. (Note: I'm interested in + examples of what some windows apps use for a logs dir.) + + OPINION: This function appends "Logs" to the `CSIDL_LOCAL_APPDATA` + value for Windows and appends "log" to the user cache dir for Unix. + This can be disabled with the `opinion=False` option. + """ + if system == "darwin": + path = os.path.join( + os.path.expanduser('~/Library/Logs'), + appname) + elif system == "win32": + path = user_data_dir(appname, appauthor, version) + version = False + if opinion: + path = os.path.join(path, "Logs") + else: + path = user_cache_dir(appname, appauthor, version) + version = False + if opinion: + path = os.path.join(path, "log") + if appname and version: + path = os.path.join(path, version) + return path + + +class AppDirs(object): + """Convenience wrapper for getting application dirs.""" + def __init__(self, appname=None, appauthor=None, version=None, + roaming=False, multipath=False): + self.appname = appname + self.appauthor = appauthor + self.version = version + self.roaming = roaming + self.multipath = multipath + + @property + def user_data_dir(self): + return user_data_dir(self.appname, self.appauthor, + version=self.version, roaming=self.roaming) + + @property + def site_data_dir(self): + return site_data_dir(self.appname, self.appauthor, + version=self.version, multipath=self.multipath) + + @property + def user_config_dir(self): + return user_config_dir(self.appname, self.appauthor, + version=self.version, roaming=self.roaming) + + @property + def site_config_dir(self): + return site_config_dir(self.appname, self.appauthor, + version=self.version, multipath=self.multipath) + + @property + def user_cache_dir(self): + return user_cache_dir(self.appname, self.appauthor, + version=self.version) + + @property + def user_state_dir(self): + return user_state_dir(self.appname, self.appauthor, + version=self.version) + + @property + def user_log_dir(self): + return user_log_dir(self.appname, self.appauthor, + version=self.version) + + +#---- internal support stuff + +def _get_win_folder_from_registry(csidl_name): + """This is a fallback technique at best. I'm not sure if using the + registry for this guarantees us the correct answer for all CSIDL_* + names. + """ + if PY3: + import winreg as _winreg + else: + import _winreg + + shell_folder_name = { + "CSIDL_APPDATA": "AppData", + "CSIDL_COMMON_APPDATA": "Common AppData", + "CSIDL_LOCAL_APPDATA": "Local AppData", + }[csidl_name] + + key = _winreg.OpenKey( + _winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" + ) + dir, type = _winreg.QueryValueEx(key, shell_folder_name) + return dir + + +def _get_win_folder_with_pywin32(csidl_name): + from win32com.shell import shellcon, shell + dir = shell.SHGetFolderPath(0, getattr(shellcon, csidl_name), 0, 0) + # Try to make this a unicode path because SHGetFolderPath does + # not return unicode strings when there is unicode data in the + # path. + try: + dir = unicode(dir) + + # Downgrade to short path name if have highbit chars. See + # . + has_high_char = False + for c in dir: + if ord(c) > 255: + has_high_char = True + break + if has_high_char: + try: + import win32api + dir = win32api.GetShortPathName(dir) + except ImportError: + pass + except UnicodeError: + pass + return dir + + +def _get_win_folder_with_ctypes(csidl_name): + import ctypes + + csidl_const = { + "CSIDL_APPDATA": 26, + "CSIDL_COMMON_APPDATA": 35, + "CSIDL_LOCAL_APPDATA": 28, + }[csidl_name] + + buf = ctypes.create_unicode_buffer(1024) + ctypes.windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf) + + # Downgrade to short path name if have highbit chars. See + # . + has_high_char = False + for c in buf: + if ord(c) > 255: + has_high_char = True + break + if has_high_char: + buf2 = ctypes.create_unicode_buffer(1024) + if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024): + buf = buf2 + + return buf.value + +def _get_win_folder_with_jna(csidl_name): + import array + from com.sun import jna + from com.sun.jna.platform import win32 + + buf_size = win32.WinDef.MAX_PATH * 2 + buf = array.zeros('c', buf_size) + shell = win32.Shell32.INSTANCE + shell.SHGetFolderPath(None, getattr(win32.ShlObj, csidl_name), None, win32.ShlObj.SHGFP_TYPE_CURRENT, buf) + dir = jna.Native.toString(buf.tostring()).rstrip("\0") + + # Downgrade to short path name if have highbit chars. See + # . + has_high_char = False + for c in dir: + if ord(c) > 255: + has_high_char = True + break + if has_high_char: + buf = array.zeros('c', buf_size) + kernel = win32.Kernel32.INSTANCE + if kernel.GetShortPathName(dir, buf, buf_size): + dir = jna.Native.toString(buf.tostring()).rstrip("\0") + + return dir + +if system == "win32": + try: + import win32com.shell + _get_win_folder = _get_win_folder_with_pywin32 + except ImportError: + try: + from ctypes import windll + _get_win_folder = _get_win_folder_with_ctypes + except ImportError: + try: + import com.sun.jna + _get_win_folder = _get_win_folder_with_jna + except ImportError: + _get_win_folder = _get_win_folder_from_registry + + +#---- self test code + +if __name__ == "__main__": + appname = "MyApp" + appauthor = "MyCompany" + + props = ("user_data_dir", + "user_config_dir", + "user_cache_dir", + "user_state_dir", + "user_log_dir", + "site_data_dir", + "site_config_dir") + + print("-- app dirs %s --" % __version__) + + print("-- app dirs (with optional 'version')") + dirs = AppDirs(appname, appauthor, version="1.0") + for prop in props: + print("%s: %s" % (prop, getattr(dirs, prop))) + + print("\n-- app dirs (without optional 'version')") + dirs = AppDirs(appname, appauthor) + for prop in props: + print("%s: %s" % (prop, getattr(dirs, prop))) + + print("\n-- app dirs (without optional 'appauthor')") + dirs = AppDirs(appname) + for prop in props: + print("%s: %s" % (prop, getattr(dirs, prop))) + + print("\n-- app dirs (with disabled 'appauthor')") + dirs = AppDirs(appname, appauthor=False) + for prop in props: + print("%s: %s" % (prop, getattr(dirs, prop))) diff --git a/libs/bin/guessit.exe b/libs/bin/guessit.exe index b50c3e4d..0645fb74 100644 Binary files a/libs/bin/guessit.exe and b/libs/bin/guessit.exe differ diff --git a/libs/bin/pbr.exe b/libs/bin/pbr.exe new file mode 100644 index 00000000..9021803a Binary files /dev/null and b/libs/bin/pbr.exe differ diff --git a/libs/bin/srt.exe b/libs/bin/srt.exe new file mode 100644 index 00000000..57a09da5 Binary files /dev/null and b/libs/bin/srt.exe differ diff --git a/libs/bin/subliminal.exe b/libs/bin/subliminal.exe new file mode 100644 index 00000000..51c897be Binary files /dev/null and b/libs/bin/subliminal.exe differ diff --git a/libs/bs4/__init__.py b/libs/bs4/__init__.py index aa818ae4..797a6826 100644 --- a/libs/bs4/__init__.py +++ b/libs/bs4/__init__.py @@ -21,14 +21,15 @@ http://www.crummy.com/software/BeautifulSoup/bs4/doc/ # found in the LICENSE file. __author__ = "Leonard Richardson (leonardr@segfault.org)" -__version__ = "4.5.1" -__copyright__ = "Copyright (c) 2004-2016 Leonard Richardson" +__version__ = "4.6.3" +__copyright__ = "Copyright (c) 2004-2018 Leonard Richardson" __license__ = "MIT" __all__ = ['BeautifulSoup'] import os import re +import sys import traceback import warnings @@ -50,7 +51,7 @@ from .element import ( # The very first thing we do is give a useful error if someone is # running this code under Python 3 without converting it. -'You are trying to run the Python 2 version of Beautiful Soup under Python 3. This will not work.'<>'You need to convert the code, either by installing it (`python setup.py install`) or by running 2to3 (`2to3 -w bs4`).' +'You are trying to run the Python 2 version of Beautiful Soup under Python 3. This will not work.'!='You need to convert the code, either by installing it (`python setup.py install`) or by running 2to3 (`2to3 -w bs4`).' class BeautifulSoup(Tag): """ @@ -74,7 +75,7 @@ class BeautifulSoup(Tag): like HTML's
tag), call handle_starttag and then handle_endtag. """ - ROOT_TAG_NAME = u'[document]' + ROOT_TAG_NAME = '[document]' # If the end-user gives no indication which tree builder they # want, look for one with these features. @@ -82,14 +83,46 @@ class BeautifulSoup(Tag): ASCII_SPACES = '\x20\x0a\x09\x0c\x0d' - NO_PARSER_SPECIFIED_WARNING = "No parser was explicitly specified, so I'm using the best available %(markup_type)s parser for this system (\"%(parser)s\"). This usually isn't a problem, but if you run this code on another system, or in a different virtual environment, it may use a different parser and behave differently.\n\nThe code that caused this warning is on line %(line_number)s of the file %(filename)s. To get rid of this warning, change code that looks like this:\n\n BeautifulSoup([your markup])\n\nto this:\n\n BeautifulSoup([your markup], \"%(parser)s\")\n" + NO_PARSER_SPECIFIED_WARNING = "No parser was explicitly specified, so I'm using the best available %(markup_type)s parser for this system (\"%(parser)s\"). This usually isn't a problem, but if you run this code on another system, or in a different virtual environment, it may use a different parser and behave differently.\n\nThe code that caused this warning is on line %(line_number)s of the file %(filename)s. To get rid of this warning, pass the additional argument 'features=\"%(parser)s\"' to the BeautifulSoup constructor.\n" def __init__(self, markup="", features=None, builder=None, parse_only=None, from_encoding=None, exclude_encodings=None, **kwargs): - """The Soup object is initialized as the 'root tag', and the - provided markup (which can be a string or a file-like object) - is fed into the underlying parser.""" + """Constructor. + + :param markup: A string or a file-like object representing + markup to be parsed. + + :param features: Desirable features of the parser to be used. This + may be the name of a specific parser ("lxml", "lxml-xml", + "html.parser", or "html5lib") or it may be the type of markup + to be used ("html", "html5", "xml"). It's recommended that you + name a specific parser, so that Beautiful Soup gives you the + same results across platforms and virtual environments. + + :param builder: A specific TreeBuilder to use instead of looking one + up based on `features`. You shouldn't need to use this. + + :param parse_only: A SoupStrainer. Only parts of the document + matching the SoupStrainer will be considered. This is useful + when parsing part of a document that would otherwise be too + large to fit into memory. + + :param from_encoding: A string indicating the encoding of the + document to be parsed. Pass this in if Beautiful Soup is + guessing wrongly about the document's encoding. + + :param exclude_encodings: A list of strings indicating + encodings known to be wrong. Pass this in if you don't know + the document's encoding but you know Beautiful Soup's guess is + wrong. + + :param kwargs: For backwards compatibility purposes, the + constructor accepts certain keyword arguments used in + Beautiful Soup 3. None of these arguments do anything in + Beautiful Soup 4 and there's no need to actually pass keyword + arguments into the constructor. + """ if 'convertEntities' in kwargs: warnings.warn( @@ -142,18 +175,18 @@ class BeautifulSoup(Tag): from_encoding = from_encoding or deprecated_argument( "fromEncoding", "from_encoding") - if from_encoding and isinstance(markup, unicode): + if from_encoding and isinstance(markup, str): warnings.warn("You provided Unicode markup but also provided a value for from_encoding. Your from_encoding will be ignored.") from_encoding = None if len(kwargs) > 0: - arg = kwargs.keys().pop() + arg = list(kwargs.keys()).pop() raise TypeError( "__init__() got an unexpected keyword argument '%s'" % arg) if builder is None: original_features = features - if isinstance(features, basestring): + if isinstance(features, str): features = [features] if features is None or len(features) == 0: features = self.DEFAULT_BUILDER_FEATURES @@ -171,14 +204,35 @@ class BeautifulSoup(Tag): else: markup_type = "HTML" - caller = traceback.extract_stack()[0] - filename = caller[0] - line_number = caller[1] - warnings.warn(self.NO_PARSER_SPECIFIED_WARNING % dict( - filename=filename, - line_number=line_number, - parser=builder.NAME, - markup_type=markup_type)) + # This code adapted from warnings.py so that we get the same line + # of code as our warnings.warn() call gets, even if the answer is wrong + # (as it may be in a multithreading situation). + caller = None + try: + caller = sys._getframe(1) + except ValueError: + pass + if caller: + globals = caller.f_globals + line_number = caller.f_lineno + else: + globals = sys.__dict__ + line_number= 1 + filename = globals.get('__file__') + if filename: + fnl = filename.lower() + if fnl.endswith((".pyc", ".pyo")): + filename = filename[:-1] + if filename: + # If there is no filename at all, the user is most likely in a REPL, + # and the warning is not necessary. + values = dict( + filename=filename, + line_number=line_number, + parser=builder.NAME, + markup_type=markup_type + ) + warnings.warn(self.NO_PARSER_SPECIFIED_WARNING % values, stacklevel=2) self.builder = builder self.is_xml = builder.is_xml @@ -191,13 +245,13 @@ class BeautifulSoup(Tag): markup = markup.read() elif len(markup) <= 256 and ( (isinstance(markup, bytes) and not b'<' in markup) - or (isinstance(markup, unicode) and not u'<' in markup) + or (isinstance(markup, str) and not '<' in markup) ): # Print out warnings for a couple beginner problems # involving passing non-markup to Beautiful Soup. # Beautiful Soup will still parse the input as markup, # just in case that's what the user really wants. - if (isinstance(markup, unicode) + if (isinstance(markup, str) and not os.path.supports_unicode_filenames): possible_filename = markup.encode("utf8") else: @@ -205,18 +259,18 @@ class BeautifulSoup(Tag): is_file = False try: is_file = os.path.exists(possible_filename) - except Exception, e: + except Exception as e: # This is almost certainly a problem involving # characters not valid in filenames on this # system. Just let it go. pass if is_file: - if isinstance(markup, unicode): + if isinstance(markup, str): markup = markup.encode("utf8") warnings.warn( '"%s" looks like a filename, not markup. You should' - 'probably open this file and pass the filehandle into' - 'Beautiful Soup.' % markup) + ' probably open this file and pass the filehandle into' + ' Beautiful Soup.' % markup) self._check_markup_is_url(markup) for (self.markup, self.original_encoding, self.declared_html_encoding, @@ -263,9 +317,9 @@ class BeautifulSoup(Tag): if isinstance(markup, bytes): space = b' ' cant_start_with = (b"http:", b"https:") - elif isinstance(markup, unicode): - space = u' ' - cant_start_with = (u"http:", u"https:") + elif isinstance(markup, str): + space = ' ' + cant_start_with = ("http:", "https:") else: return @@ -302,9 +356,10 @@ class BeautifulSoup(Tag): self.preserve_whitespace_tag_stack = [] self.pushTag(self) - def new_tag(self, name, namespace=None, nsprefix=None, **attrs): + def new_tag(self, name, namespace=None, nsprefix=None, attrs={}, **kwattrs): """Create a new tag associated with this soup.""" - return Tag(None, self.builder, name, namespace, nsprefix, attrs) + kwattrs.update(attrs) + return Tag(None, self.builder, name, namespace, nsprefix, kwattrs) def new_string(self, s, subclass=NavigableString): """Create a new NavigableString associated with this soup.""" @@ -336,7 +391,7 @@ class BeautifulSoup(Tag): def endData(self, containerClass=NavigableString): if self.current_data: - current_data = u''.join(self.current_data) + current_data = ''.join(self.current_data) # If whitespace is not preserved, and this string contains # nothing but ASCII spaces, replace it with a single space # or newline. @@ -490,9 +545,9 @@ class BeautifulSoup(Tag): encoding_part = '' if eventual_encoding != None: encoding_part = ' encoding="%s"' % eventual_encoding - prefix = u'\n' % encoding_part + prefix = '\n' % encoding_part else: - prefix = u'' + prefix = '' if not pretty_print: indent_level = None else: @@ -526,4 +581,4 @@ class FeatureNotFound(ValueError): if __name__ == '__main__': import sys soup = BeautifulSoup(sys.stdin) - print soup.prettify() + print(soup.prettify()) diff --git a/libs/bs4/builder/__init__.py b/libs/bs4/builder/__init__.py index 601979bf..b80ad684 100644 --- a/libs/bs4/builder/__init__.py +++ b/libs/bs4/builder/__init__.py @@ -93,7 +93,7 @@ class TreeBuilder(object): preserve_whitespace_tags = set() empty_element_tags = None # A tag will be considered an empty-element # tag when and only when it has no contents. - + # A value for these tag/attribute combinations is a space- or # comma-separated list of CDATA, rather than a single CDATA. cdata_list_attributes = {} @@ -125,7 +125,7 @@ class TreeBuilder(object): if self.empty_element_tags is None: return True return tag_name in self.empty_element_tags - + def feed(self, markup): raise NotImplementedError() @@ -160,13 +160,13 @@ class TreeBuilder(object): universal = self.cdata_list_attributes.get('*', []) tag_specific = self.cdata_list_attributes.get( tag_name.lower(), None) - for attr in attrs.keys(): + for attr in list(attrs.keys()): if attr in universal or (tag_specific and attr in tag_specific): # We have a "class"-type attribute whose string # value is a whitespace-separated list of # values. Split it into a list. value = attrs[attr] - if isinstance(value, basestring): + if isinstance(value, str): values = whitespace_re.split(value) else: # html5lib sometimes calls setAttributes twice @@ -232,9 +232,20 @@ class HTMLTreeBuilder(TreeBuilder): """ preserve_whitespace_tags = HTMLAwareEntitySubstitution.preserve_whitespace_tags - empty_element_tags = set(['br' , 'hr', 'input', 'img', 'meta', - 'spacer', 'link', 'frame', 'base']) + empty_element_tags = set([ + # These are from HTML5. + 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr', + + # These are from earlier versions of HTML and are removed in HTML5. + 'basefont', 'bgsound', 'command', 'frame', 'image', 'isindex', 'nextid', 'spacer' + ]) + # The HTML standard defines these as block-level elements. Beautiful + # Soup does not treat these elements differently from other elements, + # but it may do so eventually, and this information is available if + # you need to use it. + block_elements = set(["address", "article", "aside", "blockquote", "canvas", "dd", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hr", "li", "main", "nav", "noscript", "ol", "output", "p", "pre", "section", "table", "tfoot", "ul", "video"]) + # The HTML standard defines these attributes as containing a # space-separated list of values, not a single value. That is, # class="foo bar" means that the 'class' attribute has two values, diff --git a/libs/bs4/builder/_html5lib.py b/libs/bs4/builder/_html5lib.py index c46f8823..d9d468f5 100644 --- a/libs/bs4/builder/_html5lib.py +++ b/libs/bs4/builder/_html5lib.py @@ -6,6 +6,7 @@ __all__ = [ ] import warnings +import re from bs4.builder import ( PERMISSIVE, HTML, @@ -17,7 +18,10 @@ from bs4.element import ( whitespace_re, ) import html5lib -from html5lib.constants import namespaces +from html5lib.constants import ( + namespaces, + prefixes, + ) from bs4.element import ( Comment, Doctype, @@ -29,7 +33,7 @@ try: # Pre-0.99999999 from html5lib.treebuilders import _base as treebuilder_base new_html5lib = False -except ImportError, e: +except ImportError as e: # 0.99999999 and up from html5lib.treebuilders import base as treebuilder_base new_html5lib = True @@ -60,7 +64,7 @@ class HTML5TreeBuilder(HTMLTreeBuilder): parser = html5lib.HTMLParser(tree=self.create_treebuilder) extra_kwargs = dict() - if not isinstance(markup, unicode): + if not isinstance(markup, str): if new_html5lib: extra_kwargs['override_encoding'] = self.user_specified_encoding else: @@ -68,13 +72,13 @@ class HTML5TreeBuilder(HTMLTreeBuilder): doc = parser.parse(markup, **extra_kwargs) # Set the character encoding detected by the tokenizer. - if isinstance(markup, unicode): + if isinstance(markup, str): # We need to special-case this because html5lib sets # charEncoding to UTF-8 if it gets Unicode input. doc.original_encoding = None else: original_encoding = parser.tokenizer.stream.charEncoding[0] - if not isinstance(original_encoding, basestring): + if not isinstance(original_encoding, str): # In 0.99999999 and up, the encoding is an html5lib # Encoding object. We want to use a string for compatibility # with other tree builders. @@ -83,18 +87,22 @@ class HTML5TreeBuilder(HTMLTreeBuilder): def create_treebuilder(self, namespaceHTMLElements): self.underlying_builder = TreeBuilderForHtml5lib( - self.soup, namespaceHTMLElements) + namespaceHTMLElements, self.soup) return self.underlying_builder def test_fragment_to_document(self, fragment): """See `TreeBuilder`.""" - return u'%s' % fragment + return '%s' % fragment class TreeBuilderForHtml5lib(treebuilder_base.TreeBuilder): - def __init__(self, soup, namespaceHTMLElements): - self.soup = soup + def __init__(self, namespaceHTMLElements, soup=None): + if soup: + self.soup = soup + else: + from bs4 import BeautifulSoup + self.soup = BeautifulSoup("", "html.parser") super(TreeBuilderForHtml5lib, self).__init__(namespaceHTMLElements) def documentClass(self): @@ -117,7 +125,8 @@ class TreeBuilderForHtml5lib(treebuilder_base.TreeBuilder): return TextNode(Comment(data), self.soup) def fragmentClass(self): - self.soup = BeautifulSoup("") + from bs4 import BeautifulSoup + self.soup = BeautifulSoup("", "html.parser") self.soup.name = "[document_fragment]" return Element(self.soup, self.soup, None) @@ -131,6 +140,56 @@ class TreeBuilderForHtml5lib(treebuilder_base.TreeBuilder): def getFragment(self): return treebuilder_base.TreeBuilder.getFragment(self).element + def testSerializer(self, element): + from bs4 import BeautifulSoup + rv = [] + doctype_re = re.compile(r'^(.*?)(?: PUBLIC "(.*?)"(?: "(.*?)")?| SYSTEM "(.*?)")?$') + + def serializeElement(element, indent=0): + if isinstance(element, BeautifulSoup): + pass + if isinstance(element, Doctype): + m = doctype_re.match(element) + if m: + name = m.group(1) + if m.lastindex > 1: + publicId = m.group(2) or "" + systemId = m.group(3) or m.group(4) or "" + rv.append("""|%s""" % + (' ' * indent, name, publicId, systemId)) + else: + rv.append("|%s" % (' ' * indent, name)) + else: + rv.append("|%s" % (' ' * indent,)) + elif isinstance(element, Comment): + rv.append("|%s" % (' ' * indent, element)) + elif isinstance(element, NavigableString): + rv.append("|%s\"%s\"" % (' ' * indent, element)) + else: + if element.namespace: + name = "%s %s" % (prefixes[element.namespace], + element.name) + else: + name = element.name + rv.append("|%s<%s>" % (' ' * indent, name)) + if element.attrs: + attributes = [] + for name, value in list(element.attrs.items()): + if isinstance(name, NamespacedAttribute): + name = "%s %s" % (prefixes[name.namespace], name.name) + if isinstance(value, list): + value = " ".join(value) + attributes.append((name, value)) + + for name, value in sorted(attributes): + rv.append('|%s%s="%s"' % (' ' * (indent + 2), name, value)) + indent += 2 + for child in element.children: + serializeElement(child, indent) + serializeElement(element, 0) + + return "\n".join(rv) + class AttrList(object): def __init__(self, element): self.element = element @@ -170,7 +229,7 @@ class Element(treebuilder_base.Node): def appendChild(self, node): string_child = child = None - if isinstance(node, basestring): + if isinstance(node, str): # Some other piece of code decided to pass in a string # instead of creating a TextElement object to contain the # string. @@ -182,10 +241,12 @@ class Element(treebuilder_base.Node): child = node elif node.element.__class__ == NavigableString: string_child = child = node.element + node.parent = self else: child = node.element + node.parent = self - if not isinstance(child, basestring) and child.parent is not None: + if not isinstance(child, str) and child.parent is not None: node.element.extract() if (string_child and self.element.contents @@ -198,7 +259,7 @@ class Element(treebuilder_base.Node): old_element.replace_with(new_element) self.soup._most_recent_element = new_element else: - if isinstance(node, basestring): + if isinstance(node, str): # Create a brand new NavigableString from this string. child = self.soup.new_string(node) @@ -221,6 +282,8 @@ class Element(treebuilder_base.Node): most_recent_element=most_recent_element) def getAttributes(self): + if isinstance(self.element, Comment): + return {} return AttrList(self.element) def setAttributes(self, attributes): @@ -236,7 +299,7 @@ class Element(treebuilder_base.Node): self.soup.builder._replace_cdata_list_attribute_values( self.name, attributes) - for name, value in attributes.items(): + for name, value in list(attributes.items()): self.element[name] = value # The attributes may contain variables that need substitution. @@ -248,11 +311,11 @@ class Element(treebuilder_base.Node): attributes = property(getAttributes, setAttributes) def insertText(self, data, insertBefore=None): + text = TextNode(self.soup.new_string(data), self.soup) if insertBefore: - text = TextNode(self.soup.new_string(data), self.soup) - self.insertBefore(data, insertBefore) + self.insertBefore(text, insertBefore) else: - self.appendChild(data) + self.appendChild(text) def insertBefore(self, node, refNode): index = self.element.index(refNode.element) @@ -274,6 +337,7 @@ class Element(treebuilder_base.Node): # print "MOVE", self.element.contents # print "FROM", self.element # print "TO", new_parent.element + element = self.element new_parent_element = new_parent.element # Determine what this tag's next_element will be once all the children @@ -292,7 +356,6 @@ class Element(treebuilder_base.Node): new_parents_last_descendant_next_element = new_parent_element.next_element to_append = element.contents - append_after = new_parent_element.contents if len(to_append) > 0: # Set the first child's previous_element and previous_sibling # to elements within the new parent @@ -309,12 +372,19 @@ class Element(treebuilder_base.Node): if new_parents_last_child: new_parents_last_child.next_sibling = first_child - # Fix the last child's next_element and next_sibling - last_child = to_append[-1] - last_child.next_element = new_parents_last_descendant_next_element + # Find the very last element being moved. It is now the + # parent's last descendant. It has no .next_sibling and + # its .next_element is whatever the previous last + # descendant had. + last_childs_last_descendant = to_append[-1]._last_descendant(False, True) + + last_childs_last_descendant.next_element = new_parents_last_descendant_next_element if new_parents_last_descendant_next_element: - new_parents_last_descendant_next_element.previous_element = last_child - last_child.next_sibling = None + # TODO: This code has no test coverage and I'm not sure + # how to get html5lib to go through this path, but it's + # just the other side of the previous line. + new_parents_last_descendant_next_element.previous_element = last_childs_last_descendant + last_childs_last_descendant.next_sibling = None for child in to_append: child.parent = new_parent_element diff --git a/libs/bs4/builder/_htmlparser.py b/libs/bs4/builder/_htmlparser.py index 823ca15a..7ae60272 100644 --- a/libs/bs4/builder/_htmlparser.py +++ b/libs/bs4/builder/_htmlparser.py @@ -1,3 +1,4 @@ +# encoding: utf-8 """Use the HTMLParser library to parse HTML files that aren't too bad.""" # Use of this source code is governed by a BSD-style license that can be @@ -7,11 +8,11 @@ __all__ = [ 'HTMLParserTreeBuilder', ] -from HTMLParser import HTMLParser +from html.parser import HTMLParser try: - from HTMLParser import HTMLParseError -except ImportError, e: + from html.parser import HTMLParseError +except ImportError as e: # HTMLParseError is removed in Python 3.5. Since it can never be # thrown in 3.5, we can just define our own class as a placeholder. class HTMLParseError(Exception): @@ -52,7 +53,42 @@ from bs4.builder import ( HTMLPARSER = 'html.parser' class BeautifulSoupHTMLParser(HTMLParser): - def handle_starttag(self, name, attrs): + + def __init__(self, *args, **kwargs): + HTMLParser.__init__(self, *args, **kwargs) + + # Keep a list of empty-element tags that were encountered + # without an explicit closing tag. If we encounter a closing tag + # of this type, we'll associate it with one of those entries. + # + # This isn't a stack because we don't care about the + # order. It's a list of closing tags we've already handled and + # will ignore, assuming they ever show up. + self.already_closed_empty_element = [] + + def error(self, msg): + """In Python 3, HTMLParser subclasses must implement error(), although this + requirement doesn't appear to be documented. + + In Python 2, HTMLParser implements error() as raising an exception. + + In any event, this method is called only on very strange markup and our best strategy + is to pretend it didn't happen and keep going. + """ + warnings.warn(msg) + + def handle_startendtag(self, name, attrs): + # This is only called when the markup looks like + # . + + # is_startend() tells handle_starttag not to close the tag + # just because its name matches a known empty-element tag. We + # know that this is an empty-element tag and we want to call + # handle_endtag ourselves. + tag = self.handle_starttag(name, attrs, handle_empty_element=False) + self.handle_endtag(name) + + def handle_starttag(self, name, attrs, handle_empty_element=True): # XXX namespace attr_dict = {} for key, value in attrs: @@ -62,10 +98,34 @@ class BeautifulSoupHTMLParser(HTMLParser): value = '' attr_dict[key] = value attrvalue = '""' - self.soup.handle_starttag(name, None, None, attr_dict) + #print "START", name + tag = self.soup.handle_starttag(name, None, None, attr_dict) + if tag and tag.is_empty_element and handle_empty_element: + # Unlike other parsers, html.parser doesn't send separate end tag + # events for empty-element tags. (It's handled in + # handle_startendtag, but only if the original markup looked like + # .) + # + # So we need to call handle_endtag() ourselves. Since we + # know the start event is identical to the end event, we + # don't want handle_endtag() to cross off any previous end + # events for tags of this name. + self.handle_endtag(name, check_already_closed=False) - def handle_endtag(self, name): - self.soup.handle_endtag(name) + # But we might encounter an explicit closing tag for this tag + # later on. If so, we want to ignore it. + self.already_closed_empty_element.append(name) + + def handle_endtag(self, name, check_already_closed=True): + #print "END", name + if check_already_closed and name in self.already_closed_empty_element: + # This is a redundant end tag for an empty-element tag. + # We've already called handle_endtag() for it, so just + # check it off the list. + # print "ALREADY CLOSED", name + self.already_closed_empty_element.remove(name) + else: + self.soup.handle_endtag(name) def handle_data(self, data): self.soup.handle_data(data) @@ -81,11 +141,26 @@ class BeautifulSoupHTMLParser(HTMLParser): else: real_name = int(name) - try: - data = unichr(real_name) - except (ValueError, OverflowError), e: - data = u"\N{REPLACEMENT CHARACTER}" - + data = None + if real_name < 256: + # HTML numeric entities are supposed to reference Unicode + # code points, but sometimes they reference code points in + # some other encoding (ahem, Windows-1252). E.g. “ + # instead of É for LEFT DOUBLE QUOTATION MARK. This + # code tries to detect this situation and compensate. + for encoding in (self.soup.original_encoding, 'windows-1252'): + if not encoding: + continue + try: + data = bytearray([real_name]).decode(encoding) + except UnicodeDecodeError as e: + pass + if not data: + try: + data = chr(real_name) + except (ValueError, OverflowError) as e: + pass + data = data or "\N{REPLACEMENT CHARACTER}" self.handle_data(data) def handle_entityref(self, name): @@ -93,7 +168,12 @@ class BeautifulSoupHTMLParser(HTMLParser): if character is not None: data = character else: - data = "&%s;" % name + # If this were XML, it would be ambiguous whether "&foo" + # was an character entity reference with a missing + # semicolon or the literal string "&foo". Since this is + # HTML, we have a complete list of all character entity references, + # and this one wasn't found, so assume it's the literal string "&foo". + data = "&%s" % name self.handle_data(data) def handle_comment(self, data): @@ -148,7 +228,7 @@ class HTMLParserTreeBuilder(HTMLTreeBuilder): declared within markup, whether any characters had to be replaced with REPLACEMENT CHARACTER). """ - if isinstance(markup, unicode): + if isinstance(markup, str): yield (markup, None, None, False) return @@ -165,10 +245,12 @@ class HTMLParserTreeBuilder(HTMLTreeBuilder): parser.soup = self.soup try: parser.feed(markup) - except HTMLParseError, e: + parser.close() + except HTMLParseError as e: warnings.warn(RuntimeWarning( "Python's built-in HTMLParser cannot parse the given document. This is not a bug in Beautiful Soup. The best solution is to install an external parser (lxml or html5lib), and use Beautiful Soup with that parser. See http://www.crummy.com/software/BeautifulSoup/bs4/doc/#installing-a-parser for help.")) raise e + parser.already_closed_empty_element = [] # Patch 3.2 versions of HTMLParser earlier than 3.2.3 to use some # 3.2.3 code. This ensures they don't treat markup like

as a diff --git a/libs/bs4/builder/_lxml.py b/libs/bs4/builder/_lxml.py index d2ca2872..0dc9affe 100644 --- a/libs/bs4/builder/_lxml.py +++ b/libs/bs4/builder/_lxml.py @@ -5,9 +5,13 @@ __all__ = [ 'LXMLTreeBuilder', ] +try: + from collections.abc import Callable # Python 3.6 +except ImportError as e: + from collections import Callable + from io import BytesIO -from StringIO import StringIO -import collections +from io import StringIO from lxml import etree from bs4.element import ( Comment, @@ -58,7 +62,7 @@ class LXMLTreeBuilderForXML(TreeBuilder): # Use the default parser. parser = self.default_parser(encoding) - if isinstance(parser, collections.Callable): + if isinstance(parser, Callable): # Instantiate the parser with default arguments parser = parser(target=self, strip_cdata=False, encoding=encoding) return parser @@ -101,12 +105,12 @@ class LXMLTreeBuilderForXML(TreeBuilder): else: self.processing_instruction_class = XMLProcessingInstruction - if isinstance(markup, unicode): + if isinstance(markup, str): # We were given Unicode. Maybe lxml can parse Unicode on # this system? yield markup, None, document_declared_encoding, False - if isinstance(markup, unicode): + if isinstance(markup, str): # No, apparently not. Convert the Unicode to UTF-8 and # tell lxml to parse it as UTF-8. yield (markup.encode("utf8"), "utf8", @@ -121,7 +125,7 @@ class LXMLTreeBuilderForXML(TreeBuilder): def feed(self, markup): if isinstance(markup, bytes): markup = BytesIO(markup) - elif isinstance(markup, unicode): + elif isinstance(markup, str): markup = StringIO(markup) # Call feed() at least once, even if the markup is empty, @@ -136,7 +140,7 @@ class LXMLTreeBuilderForXML(TreeBuilder): if len(data) != 0: self.parser.feed(data) self.parser.close() - except (UnicodeDecodeError, LookupError, etree.ParserError), e: + except (UnicodeDecodeError, LookupError, etree.ParserError) as e: raise ParserRejectedMarkup(str(e)) def close(self): @@ -147,19 +151,19 @@ class LXMLTreeBuilderForXML(TreeBuilder): attrs = dict(attrs) nsprefix = None # Invert each namespace map as it comes in. - if len(self.nsmaps) > 1: - # There are no new namespaces for this tag, but - # non-default namespaces are in play, so we need a - # separate tag stack to know when they end. - self.nsmaps.append(None) + if len(nsmap) == 0 and len(self.nsmaps) > 1: + # There are no new namespaces for this tag, but + # non-default namespaces are in play, so we need a + # separate tag stack to know when they end. + self.nsmaps.append(None) elif len(nsmap) > 0: # A new namespace mapping has come into play. - inverted_nsmap = dict((value, key) for key, value in nsmap.items()) + inverted_nsmap = dict((value, key) for key, value in list(nsmap.items())) self.nsmaps.append(inverted_nsmap) # Also treat the namespace mapping as a set of attributes on the # tag, so we can recreate it later. attrs = attrs.copy() - for prefix, namespace in nsmap.items(): + for prefix, namespace in list(nsmap.items()): attribute = NamespacedAttribute( "xmlns", prefix, "http://www.w3.org/2000/xmlns/") attrs[attribute] = namespace @@ -168,7 +172,7 @@ class LXMLTreeBuilderForXML(TreeBuilder): # from lxml with namespaces attached to their names, and # turn then into NamespacedAttribute objects. new_attrs = {} - for attr, value in attrs.items(): + for attr, value in list(attrs.items()): namespace, attr = self._getNsTag(attr) if namespace is None: new_attrs[attr] = value @@ -228,7 +232,7 @@ class LXMLTreeBuilderForXML(TreeBuilder): def test_fragment_to_document(self, fragment): """See `TreeBuilder`.""" - return u'\n%s' % fragment + return '\n%s' % fragment class LXMLTreeBuilder(HTMLTreeBuilder, LXMLTreeBuilderForXML): @@ -249,10 +253,10 @@ class LXMLTreeBuilder(HTMLTreeBuilder, LXMLTreeBuilderForXML): self.parser = self.parser_for(encoding) self.parser.feed(markup) self.parser.close() - except (UnicodeDecodeError, LookupError, etree.ParserError), e: + except (UnicodeDecodeError, LookupError, etree.ParserError) as e: raise ParserRejectedMarkup(str(e)) def test_fragment_to_document(self, fragment): """See `TreeBuilder`.""" - return u'%s' % fragment + return '%s' % fragment diff --git a/libs/bs4/dammit.py b/libs/bs4/dammit.py index 2bf67f7f..ae6d4ad8 100644 --- a/libs/bs4/dammit.py +++ b/libs/bs4/dammit.py @@ -11,7 +11,7 @@ XML or HTML to reflect a new encoding; that's the tree builder's job. __license__ = "MIT" import codecs -from htmlentitydefs import codepoint2name +from html.entities import codepoint2name import re import logging import string @@ -46,9 +46,9 @@ except ImportError: pass xml_encoding_re = re.compile( - '^<\?.*encoding=[\'"](.*?)[\'"].*\?>'.encode(), re.I) + '^<\\?.*encoding=[\'"](.*?)[\'"].*\\?>'.encode(), re.I) html_meta_re = re.compile( - '<\s*meta[^>]+charset\s*=\s*["\']?([^>]*?)[ /;\'">]'.encode(), re.I) + '<\\s*meta[^>]+charset\\s*=\\s*["\']?([^>]*?)[ /;\'">]'.encode(), re.I) class EntitySubstitution(object): @@ -59,7 +59,7 @@ class EntitySubstitution(object): reverse_lookup = {} characters_for_re = [] for codepoint, name in list(codepoint2name.items()): - character = unichr(codepoint) + character = chr(codepoint) if codepoint != 34: # There's no point in turning the quotation mark into # ", unless it happens within an attribute value, which @@ -82,7 +82,7 @@ class EntitySubstitution(object): } BARE_AMPERSAND_OR_BRACKET = re.compile("([<>]|" - "&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)" + "&(?!#\\d+;|#x[0-9a-fA-F]+;|\\w+;)" ")") AMPERSAND_OR_BRACKET = re.compile("([<>&])") @@ -274,7 +274,7 @@ class EncodingDetector: def strip_byte_order_mark(cls, data): """If a byte-order mark is present, strip it and return the encoding it implies.""" encoding = None - if isinstance(data, unicode): + if isinstance(data, str): # Unicode data cannot have a byte-order mark. return data, encoding if (len(data) >= 4) and (data[:2] == b'\xfe\xff') \ @@ -310,7 +310,7 @@ class EncodingDetector: else: xml_endpos = 1024 html_endpos = max(2048, int(len(markup) * 0.05)) - + declared_encoding = None declared_encoding_match = xml_encoding_re.search(markup, endpos=xml_endpos) if not declared_encoding_match and is_html: @@ -352,9 +352,9 @@ class UnicodeDammit: markup, override_encodings, is_html, exclude_encodings) # Short-circuit if the data is in Unicode to begin with. - if isinstance(markup, unicode) or markup == '': + if isinstance(markup, str) or markup == '': self.markup = markup - self.unicode_markup = unicode(markup) + self.unicode_markup = str(markup) self.original_encoding = None return @@ -438,7 +438,7 @@ class UnicodeDammit: def _to_unicode(self, data, encoding, errors="strict"): '''Given a string and its encoding, decodes the string into Unicode. %encoding is a string recognized by encodings.aliases''' - return unicode(data, encoding, errors) + return str(data, encoding, errors) @property def declared_html_encoding(self): @@ -736,7 +736,7 @@ class UnicodeDammit: 0xde : b'\xc3\x9e', # Þ 0xdf : b'\xc3\x9f', # ß 0xe0 : b'\xc3\xa0', # à - 0xe1 : b'\xa1', # á + 0xe1 : b'\xa1', # á 0xe2 : b'\xc3\xa2', # â 0xe3 : b'\xc3\xa3', # ã 0xe4 : b'\xc3\xa4', # ä diff --git a/libs/bs4/diagnose.py b/libs/bs4/diagnose.py index 8768332f..f97c7799 100644 --- a/libs/bs4/diagnose.py +++ b/libs/bs4/diagnose.py @@ -5,8 +5,8 @@ __license__ = "MIT" import cProfile -from StringIO import StringIO -from HTMLParser import HTMLParser +from io import StringIO +from html.parser import HTMLParser import bs4 from bs4 import BeautifulSoup, __version__ from bs4.builder import builder_registry @@ -22,8 +22,8 @@ import cProfile def diagnose(data): """Diagnostic suite for isolating common problems.""" - print "Diagnostic running on Beautiful Soup %s" % __version__ - print "Python version %s" % sys.version + print("Diagnostic running on Beautiful Soup %s" % __version__) + print("Python version %s" % sys.version) basic_parsers = ["html.parser", "html5lib", "lxml"] for name in basic_parsers: @@ -32,16 +32,16 @@ def diagnose(data): break else: basic_parsers.remove(name) - print ( + print(( "I noticed that %s is not installed. Installing it may help." % - name) + name)) if 'lxml' in basic_parsers: - basic_parsers.append(["lxml", "xml"]) + basic_parsers.append("lxml-xml") try: from lxml import etree - print "Found lxml version %s" % ".".join(map(str,etree.LXML_VERSION)) - except ImportError, e: + print("Found lxml version %s" % ".".join(map(str,etree.LXML_VERSION))) + except ImportError as e: print ( "lxml is not installed or couldn't be imported.") @@ -49,37 +49,43 @@ def diagnose(data): if 'html5lib' in basic_parsers: try: import html5lib - print "Found html5lib version %s" % html5lib.__version__ - except ImportError, e: + print("Found html5lib version %s" % html5lib.__version__) + except ImportError as e: print ( "html5lib is not installed or couldn't be imported.") if hasattr(data, 'read'): data = data.read() - elif os.path.exists(data): - print '"%s" looks like a filename. Reading data from the file.' % data - with open(data) as fp: - data = fp.read() elif data.startswith("http:") or data.startswith("https:"): - print '"%s" looks like a URL. Beautiful Soup is not an HTTP client.' % data - print "You need to use some other library to get the document behind the URL, and feed that document to Beautiful Soup." + print('"%s" looks like a URL. Beautiful Soup is not an HTTP client.' % data) + print("You need to use some other library to get the document behind the URL, and feed that document to Beautiful Soup.") return - print + else: + try: + if os.path.exists(data): + print('"%s" looks like a filename. Reading data from the file.' % data) + with open(data) as fp: + data = fp.read() + except ValueError: + # This can happen on some platforms when the 'filename' is + # too long. Assume it's data and not a filename. + pass + print() for parser in basic_parsers: - print "Trying to parse your markup with %s" % parser + print("Trying to parse your markup with %s" % parser) success = False try: - soup = BeautifulSoup(data, parser) + soup = BeautifulSoup(data, features=parser) success = True - except Exception, e: - print "%s could not parse the markup." % parser + except Exception as e: + print("%s could not parse the markup." % parser) traceback.print_exc() if success: - print "Here's what %s did with the markup:" % parser - print soup.prettify() + print("Here's what %s did with the markup:" % parser) + print(soup.prettify()) - print "-" * 80 + print("-" * 80) def lxml_trace(data, html=True, **kwargs): """Print out the lxml events that occur during parsing. @@ -89,7 +95,7 @@ def lxml_trace(data, html=True, **kwargs): """ from lxml import etree for event, element in etree.iterparse(StringIO(data), html=html, **kwargs): - print("%s, %4s, %s" % (event, element.tag, element.text)) + print(("%s, %4s, %s" % (event, element.tag, element.text))) class AnnouncingParser(HTMLParser): """Announces HTMLParser parse events, without doing anything else.""" @@ -171,9 +177,9 @@ def rdoc(num_elements=1000): def benchmark_parsers(num_elements=100000): """Very basic head-to-head performance benchmark.""" - print "Comparative parser benchmark on Beautiful Soup %s" % __version__ + print("Comparative parser benchmark on Beautiful Soup %s" % __version__) data = rdoc(num_elements) - print "Generated a large invalid HTML document (%d bytes)." % len(data) + print("Generated a large invalid HTML document (%d bytes)." % len(data)) for parser in ["lxml", ["lxml", "html"], "html5lib", "html.parser"]: success = False @@ -182,24 +188,24 @@ def benchmark_parsers(num_elements=100000): soup = BeautifulSoup(data, parser) b = time.time() success = True - except Exception, e: - print "%s could not parse the markup." % parser + except Exception as e: + print("%s could not parse the markup." % parser) traceback.print_exc() if success: - print "BS4+%s parsed the markup in %.2fs." % (parser, b-a) + print("BS4+%s parsed the markup in %.2fs." % (parser, b-a)) from lxml import etree a = time.time() etree.HTML(data) b = time.time() - print "Raw lxml parsed the markup in %.2fs." % (b-a) + print("Raw lxml parsed the markup in %.2fs." % (b-a)) import html5lib parser = html5lib.HTMLParser() a = time.time() parser.parse(data) b = time.time() - print "Raw html5lib parsed the markup in %.2fs." % (b-a) + print("Raw html5lib parsed the markup in %.2fs." % (b-a)) def profile(num_elements=100000, parser="lxml"): diff --git a/libs/bs4/element.py b/libs/bs4/element.py index b100d18b..d9389052 100644 --- a/libs/bs4/element.py +++ b/libs/bs4/element.py @@ -2,7 +2,10 @@ # found in the LICENSE file. __license__ = "MIT" -import collections +try: + from collections.abc import Callable # Python 3.6 +except ImportError as e: + from collections import Callable import re import shlex import sys @@ -12,7 +15,7 @@ from bs4.dammit import EntitySubstitution DEFAULT_OUTPUT_ENCODING = "utf-8" PY3K = (sys.version_info[0] > 2) -whitespace_re = re.compile("\s+") +whitespace_re = re.compile(r"\s+") def _alias(attr): """Alias one attribute name to another for backward compatibility""" @@ -26,22 +29,22 @@ def _alias(attr): return alias -class NamespacedAttribute(unicode): +class NamespacedAttribute(str): def __new__(cls, prefix, name, namespace=None): if name is None: - obj = unicode.__new__(cls, prefix) + obj = str.__new__(cls, prefix) elif prefix is None: # Not really namespaced. - obj = unicode.__new__(cls, name) + obj = str.__new__(cls, name) else: - obj = unicode.__new__(cls, prefix + ":" + name) + obj = str.__new__(cls, prefix + ":" + name) obj.prefix = prefix obj.name = name obj.namespace = namespace return obj -class AttributeValueWithCharsetSubstitution(unicode): +class AttributeValueWithCharsetSubstitution(str): """A stand-in object for a character encoding specified in HTML.""" class CharsetMetaAttributeValue(AttributeValueWithCharsetSubstitution): @@ -52,7 +55,7 @@ class CharsetMetaAttributeValue(AttributeValueWithCharsetSubstitution): """ def __new__(cls, original_value): - obj = unicode.__new__(cls, original_value) + obj = str.__new__(cls, original_value) obj.original_value = original_value return obj @@ -69,15 +72,15 @@ class ContentMetaAttributeValue(AttributeValueWithCharsetSubstitution): The value of the 'content' attribute will be one of these objects. """ - CHARSET_RE = re.compile("((^|;)\s*charset=)([^;]*)", re.M) + CHARSET_RE = re.compile(r"((^|;)\s*charset=)([^;]*)", re.M) def __new__(cls, original_value): match = cls.CHARSET_RE.search(original_value) if match is None: # No substitution necessary. - return unicode.__new__(unicode, original_value) + return str.__new__(str, original_value) - obj = unicode.__new__(cls, original_value) + obj = str.__new__(cls, original_value) obj.original_value = original_value return obj @@ -123,6 +126,41 @@ class HTMLAwareEntitySubstitution(EntitySubstitution): return cls._substitute_if_appropriate( ns, EntitySubstitution.substitute_xml) +class Formatter(object): + """Contains information about how to format a parse tree.""" + + # By default, represent void elements as rather than + void_element_close_prefix = '/' + + def substitute_entities(self, *args, **kwargs): + """Transform certain characters into named entities.""" + raise NotImplementedError() + +class HTMLFormatter(Formatter): + """The default HTML formatter.""" + def substitute(self, *args, **kwargs): + return HTMLAwareEntitySubstitution.substitute_html(*args, **kwargs) + +class MinimalHTMLFormatter(Formatter): + """A minimal HTML formatter.""" + def substitute(self, *args, **kwargs): + return HTMLAwareEntitySubstitution.substitute_xml(*args, **kwargs) + +class HTML5Formatter(HTMLFormatter): + """An HTML formatter that omits the slash in a void tag.""" + void_element_close_prefix = None + +class XMLFormatter(Formatter): + """Substitute only the essential XML entities.""" + def substitute(self, *args, **kwargs): + return EntitySubstitution.substitute_xml(*args, **kwargs) + +class HTMLXMLFormatter(Formatter): + """Format XML using HTML rules.""" + def substitute(self, *args, **kwargs): + return HTMLAwareEntitySubstitution.substitute_html(*args, **kwargs) + + class PageElement(object): """Contains the navigational information for some part of the page (either a tag or a piece of text)""" @@ -132,39 +170,48 @@ class PageElement(object): # # "html" - All Unicode characters with corresponding HTML entities # are converted to those entities on output. + # "html5" - The same as "html", but empty void tags are represented as + # rather than # "minimal" - Bare ampersands and angle brackets are converted to # XML entities: & < > # None - The null formatter. Unicode characters are never # converted to entities. This is not recommended, but it's # faster than "minimal". - # A function - This function will be called on every string that + # A callable function - it will be called on every string that needs to undergo entity substitution. + # A Formatter instance - Formatter.substitute(string) will be called on every string that # needs to undergo entity substitution. # - # In an HTML document, the default "html" and "minimal" functions - # will leave the contents of """) [soup.script.extract() for i in soup.find_all("script")] - self.assertEqual("\n\n\n", unicode(soup.body)) + self.assertEqual("\n\n\n", str(soup.body)) def test_extract_works_when_element_is_surrounded_by_identical_strings(self): @@ -1184,7 +1206,7 @@ class TestElementObjects(SoupTest): tag = soup.bTag self.assertEqual(soup.b, tag) self.assertEqual( - '.bTag is deprecated, use .find("b") instead.', + '.bTag is deprecated, use .find("b") instead. If you really were looking for a tag called bTag, use .find("bTag")', str(w[0].message)) def test_has_attr(self): @@ -1284,6 +1306,10 @@ class TestCDAtaListAttributes(SoupTest): soup = self.soup("") self.assertEqual(b'', soup.a.encode()) + def test_get_attribute_list(self): + soup = self.soup("") + self.assertEqual(['abc def'], soup.a.get_attribute_list('id')) + def test_accept_charset(self): soup = self.soup('
') self.assertEqual(['ISO-8859-1', 'UTF-8'], soup.form['accept-charset']) @@ -1343,19 +1369,19 @@ class TestPersistence(SoupTest): soup = BeautifulSoup(b'

 

', 'html.parser') encoding = soup.original_encoding copy = soup.__copy__() - self.assertEqual(u"

 

", unicode(copy)) + self.assertEqual("

 

", str(copy)) self.assertEqual(encoding, copy.original_encoding) def test_unicode_pickle(self): # A tree containing Unicode characters can be pickled. - html = u"\N{SNOWMAN}" + html = "\N{SNOWMAN}" soup = self.soup(html) dumped = pickle.dumps(soup, pickle.HIGHEST_PROTOCOL) loaded = pickle.loads(dumped) self.assertEqual(loaded.decode(), soup.decode()) def test_copy_navigablestring_is_not_attached_to_tree(self): - html = u"Foo
Bar" + html = "FooBar" soup = self.soup(html) s1 = soup.find(string="Foo") s2 = copy.copy(s1) @@ -1367,7 +1393,7 @@ class TestPersistence(SoupTest): self.assertEqual(None, s2.previous_element) def test_copy_navigablestring_subclass_has_same_type(self): - html = u"" + html = "" soup = self.soup(html) s1 = soup.string s2 = copy.copy(s1) @@ -1375,19 +1401,19 @@ class TestPersistence(SoupTest): self.assertTrue(isinstance(s2, Comment)) def test_copy_entire_soup(self): - html = u"
FooBar
end" + html = "
FooBar
end" soup = self.soup(html) soup_copy = copy.copy(soup) self.assertEqual(soup, soup_copy) def test_copy_tag_copies_contents(self): - html = u"
FooBar
end" + html = "
FooBar
end" soup = self.soup(html) div = soup.div div_copy = copy.copy(div) # The two tags look the same, and evaluate to equal. - self.assertEqual(unicode(div), unicode(div_copy)) + self.assertEqual(str(div), str(div_copy)) self.assertEqual(div, div_copy) # But they're not the same object. @@ -1403,67 +1429,75 @@ class TestPersistence(SoupTest): class TestSubstitutions(SoupTest): def test_default_formatter_is_minimal(self): - markup = u"<<Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!>>" + markup = "<<Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!>>" soup = self.soup(markup) decoded = soup.decode(formatter="minimal") # The < is converted back into < but the e-with-acute is left alone. self.assertEqual( decoded, self.document_for( - u"<<Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!>>")) + "<<Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!>>")) def test_formatter_html(self): - markup = u"<<Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!>>" + markup = "
<<Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!>>" soup = self.soup(markup) decoded = soup.decode(formatter="html") self.assertEqual( decoded, - self.document_for("<<Sacré bleu!>>")) + self.document_for("
<<Sacré bleu!>>")) + def test_formatter_html5(self): + markup = "
<<Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!>>" + soup = self.soup(markup) + decoded = soup.decode(formatter="html5") + self.assertEqual( + decoded, + self.document_for("
<<Sacré bleu!>>")) + def test_formatter_minimal(self): - markup = u"<<Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!>>" + markup = "<<Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!>>" soup = self.soup(markup) decoded = soup.decode(formatter="minimal") # The < is converted back into < but the e-with-acute is left alone. self.assertEqual( decoded, self.document_for( - u"<<Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!>>")) + "<<Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!>>")) def test_formatter_null(self): - markup = u"<<Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!>>" + markup = "<<Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!>>" soup = self.soup(markup) decoded = soup.decode(formatter=None) # Neither the angle brackets nor the e-with-acute are converted. # This is not valid HTML, but it's what the user wanted. self.assertEqual(decoded, - self.document_for(u"<>")) + self.document_for("<>")) def test_formatter_custom(self): - markup = u"<foo>bar" + markup = "<foo>bar
" soup = self.soup(markup) decoded = soup.decode(formatter = lambda x: x.upper()) # Instead of normal entity conversion code, the custom # callable is called on every string. self.assertEqual( decoded, - self.document_for(u"BAR")) + self.document_for("BAR
")) def test_formatter_is_run_on_attribute_values(self): - markup = u'e' + markup = 'e' soup = self.soup(markup) a = soup.a - expect_minimal = u'e' + expect_minimal = 'e' self.assertEqual(expect_minimal, a.decode()) self.assertEqual(expect_minimal, a.decode(formatter="minimal")) - expect_html = u'e' + expect_html = 'e' self.assertEqual(expect_html, a.decode(formatter="html")) self.assertEqual(markup, a.decode(formatter=None)) - expect_upper = u'E' + expect_upper = 'E' self.assertEqual(expect_upper, a.decode(formatter=lambda x: x.upper())) def test_formatter_skips_script_tag_for_html_documents(self): @@ -1489,24 +1523,24 @@ class TestSubstitutions(SoupTest): # Everything outside the
 tag is reformatted, but everything
         # inside is left alone.
         self.assertEqual(
-            u'
\n foo\n
  \tbar\n  \n  
\n baz\n
', + '
\n foo\n
  \tbar\n  \n  
\n baz\n
', soup.div.prettify()) - def test_prettify_accepts_formatter(self): + def test_prettify_accepts_formatter_function(self): soup = BeautifulSoup("foo", 'html.parser') pretty = soup.prettify(formatter = lambda x: x.upper()) self.assertTrue("FOO" in pretty) def test_prettify_outputs_unicode_by_default(self): soup = self.soup("") - self.assertEqual(unicode, type(soup.prettify())) + self.assertEqual(str, type(soup.prettify())) def test_prettify_can_encode_data(self): soup = self.soup("") self.assertEqual(bytes, type(soup.prettify("utf-8"))) def test_html_entity_substitution_off_by_default(self): - markup = u"Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!" + markup = "Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!" soup = self.soup(markup) encoded = soup.b.encode("utf-8") self.assertEqual(encoded, markup.encode('utf-8')) @@ -1550,48 +1584,48 @@ class TestEncoding(SoupTest): """Test the ability to encode objects into strings.""" def test_unicode_string_can_be_encoded(self): - html = u"\N{SNOWMAN}" + html = "\N{SNOWMAN}" soup = self.soup(html) self.assertEqual(soup.b.string.encode("utf-8"), - u"\N{SNOWMAN}".encode("utf-8")) + "\N{SNOWMAN}".encode("utf-8")) def test_tag_containing_unicode_string_can_be_encoded(self): - html = u"\N{SNOWMAN}" + html = "\N{SNOWMAN}" soup = self.soup(html) self.assertEqual( soup.b.encode("utf-8"), html.encode("utf-8")) def test_encoding_substitutes_unrecognized_characters_by_default(self): - html = u"\N{SNOWMAN}" + html = "\N{SNOWMAN}" soup = self.soup(html) self.assertEqual(soup.b.encode("ascii"), b"") def test_encoding_can_be_made_strict(self): - html = u"\N{SNOWMAN}" + html = "\N{SNOWMAN}" soup = self.soup(html) self.assertRaises( UnicodeEncodeError, soup.encode, "ascii", errors="strict") def test_decode_contents(self): - html = u"\N{SNOWMAN}" + html = "\N{SNOWMAN}" soup = self.soup(html) - self.assertEqual(u"\N{SNOWMAN}", soup.b.decode_contents()) + self.assertEqual("\N{SNOWMAN}", soup.b.decode_contents()) def test_encode_contents(self): - html = u"\N{SNOWMAN}" + html = "\N{SNOWMAN}" soup = self.soup(html) self.assertEqual( - u"\N{SNOWMAN}".encode("utf8"), soup.b.encode_contents( + "\N{SNOWMAN}".encode("utf8"), soup.b.encode_contents( encoding="utf8")) def test_deprecated_renderContents(self): - html = u"\N{SNOWMAN}" + html = "\N{SNOWMAN}" soup = self.soup(html) self.assertEqual( - u"\N{SNOWMAN}".encode("utf8"), soup.b.renderContents()) + "\N{SNOWMAN}".encode("utf8"), soup.b.renderContents()) def test_repr(self): - html = u"\N{SNOWMAN}" + html = "\N{SNOWMAN}" soup = self.soup(html) if PY3K: self.assertEqual(html, repr(soup)) @@ -1714,7 +1748,7 @@ class TestSoupSelector(TreeTest): els = self.soup.select('title') self.assertEqual(len(els), 1) self.assertEqual(els[0].name, 'title') - self.assertEqual(els[0].contents, [u'The title']) + self.assertEqual(els[0].contents, ['The title']) def test_one_tag_many(self): els = self.soup.select('div') @@ -1760,7 +1794,7 @@ class TestSoupSelector(TreeTest): self.assertEqual(dashed[0]['id'], 'dash2') def test_dashed_tag_text(self): - self.assertEqual(self.soup.select('body > custom-dashed-tag')[0].text, u'Hello there.') + self.assertEqual(self.soup.select('body > custom-dashed-tag')[0].text, 'Hello there.') def test_select_dashed_matches_find_all(self): self.assertEqual(self.soup.select('custom-dashed-tag'), self.soup.find_all('custom-dashed-tag')) @@ -1947,12 +1981,12 @@ class TestSoupSelector(TreeTest): # Try to select first paragraph els = self.soup.select('div#inner p:nth-of-type(1)') self.assertEqual(len(els), 1) - self.assertEqual(els[0].string, u'Some text') + self.assertEqual(els[0].string, 'Some text') # Try to select third paragraph els = self.soup.select('div#inner p:nth-of-type(3)') self.assertEqual(len(els), 1) - self.assertEqual(els[0].string, u'Another') + self.assertEqual(els[0].string, 'Another') # Try to select (non-existent!) fourth paragraph els = self.soup.select('div#inner p:nth-of-type(4)') @@ -1965,7 +1999,7 @@ class TestSoupSelector(TreeTest): def test_nth_of_type_direct_descendant(self): els = self.soup.select('div#inner > p:nth-of-type(1)') self.assertEqual(len(els), 1) - self.assertEqual(els[0].string, u'Some text') + self.assertEqual(els[0].string, 'Some text') def test_id_child_selector_nth_of_type(self): self.assertSelects('#inner > p:nth-of-type(2)', ['p1']) @@ -2040,5 +2074,17 @@ class TestSoupSelector(TreeTest): def test_multiple_select_nested(self): self.assertSelects('body > div > x, y > z', ['xid', 'zidb']) + def test_select_duplicate_elements(self): + # When markup contains duplicate elements, a multiple select + # will find all of them. + markup = '
' + soup = BeautifulSoup(markup, 'html.parser') + selected = soup.select(".c1, .c2") + self.assertEqual(3, len(selected)) + # Verify that find_all finds the same elements, though because + # of an implementation detail it finds them in a different + # order. + for element in soup.find_all(class_=['c1', 'c2']): + assert element in selected diff --git a/libs/click/__init__.py b/libs/click/__init__.py new file mode 100644 index 00000000..d3c33660 --- /dev/null +++ b/libs/click/__init__.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +""" +click +~~~~~ + +Click is a simple Python module inspired by the stdlib optparse to make +writing command line scripts fun. Unlike other modules, it's based +around a simple API that does not come with too much magic and is +composable. + +:copyright: © 2014 by the Pallets team. +:license: BSD, see LICENSE.rst for more details. +""" + +# Core classes +from .core import Context, BaseCommand, Command, MultiCommand, Group, \ + CommandCollection, Parameter, Option, Argument + +# Globals +from .globals import get_current_context + +# Decorators +from .decorators import pass_context, pass_obj, make_pass_decorator, \ + command, group, argument, option, confirmation_option, \ + password_option, version_option, help_option + +# Types +from .types import ParamType, File, Path, Choice, IntRange, Tuple, \ + DateTime, STRING, INT, FLOAT, BOOL, UUID, UNPROCESSED, FloatRange + +# Utilities +from .utils import echo, get_binary_stream, get_text_stream, open_file, \ + format_filename, get_app_dir, get_os_args + +# Terminal functions +from .termui import prompt, confirm, get_terminal_size, echo_via_pager, \ + progressbar, clear, style, unstyle, secho, edit, launch, getchar, \ + pause + +# Exceptions +from .exceptions import ClickException, UsageError, BadParameter, \ + FileError, Abort, NoSuchOption, BadOptionUsage, BadArgumentUsage, \ + MissingParameter + +# Formatting +from .formatting import HelpFormatter, wrap_text + +# Parsing +from .parser import OptionParser + + +__all__ = [ + # Core classes + 'Context', 'BaseCommand', 'Command', 'MultiCommand', 'Group', + 'CommandCollection', 'Parameter', 'Option', 'Argument', + + # Globals + 'get_current_context', + + # Decorators + 'pass_context', 'pass_obj', 'make_pass_decorator', 'command', 'group', + 'argument', 'option', 'confirmation_option', 'password_option', + 'version_option', 'help_option', + + # Types + 'ParamType', 'File', 'Path', 'Choice', 'IntRange', 'Tuple', + 'DateTime', 'STRING', 'INT', 'FLOAT', 'BOOL', 'UUID', 'UNPROCESSED', + 'FloatRange', + + # Utilities + 'echo', 'get_binary_stream', 'get_text_stream', 'open_file', + 'format_filename', 'get_app_dir', 'get_os_args', + + # Terminal functions + 'prompt', 'confirm', 'get_terminal_size', 'echo_via_pager', + 'progressbar', 'clear', 'style', 'unstyle', 'secho', 'edit', 'launch', + 'getchar', 'pause', + + # Exceptions + 'ClickException', 'UsageError', 'BadParameter', 'FileError', + 'Abort', 'NoSuchOption', 'BadOptionUsage', 'BadArgumentUsage', + 'MissingParameter', + + # Formatting + 'HelpFormatter', 'wrap_text', + + # Parsing + 'OptionParser', +] + + +# Controls if click should emit the warning about the use of unicode +# literals. +disable_unicode_literals_warning = False + + +__version__ = '7.0' diff --git a/libs/click/_bashcomplete.py b/libs/click/_bashcomplete.py new file mode 100644 index 00000000..a5f1084c --- /dev/null +++ b/libs/click/_bashcomplete.py @@ -0,0 +1,293 @@ +import copy +import os +import re + +from .utils import echo +from .parser import split_arg_string +from .core import MultiCommand, Option, Argument +from .types import Choice + +try: + from collections import abc +except ImportError: + import collections as abc + +WORDBREAK = '=' + +# Note, only BASH version 4.4 and later have the nosort option. +COMPLETION_SCRIPT_BASH = ''' +%(complete_func)s() { + local IFS=$'\n' + COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \\ + COMP_CWORD=$COMP_CWORD \\ + %(autocomplete_var)s=complete $1 ) ) + return 0 +} + +%(complete_func)setup() { + local COMPLETION_OPTIONS="" + local BASH_VERSION_ARR=(${BASH_VERSION//./ }) + # Only BASH version 4.4 and later have the nosort option. + if [ ${BASH_VERSION_ARR[0]} -gt 4 ] || ([ ${BASH_VERSION_ARR[0]} -eq 4 ] && [ ${BASH_VERSION_ARR[1]} -ge 4 ]); then + COMPLETION_OPTIONS="-o nosort" + fi + + complete $COMPLETION_OPTIONS -F %(complete_func)s %(script_names)s +} + +%(complete_func)setup +''' + +COMPLETION_SCRIPT_ZSH = ''' +%(complete_func)s() { + local -a completions + local -a completions_with_descriptions + local -a response + response=("${(@f)$( env COMP_WORDS=\"${words[*]}\" \\ + COMP_CWORD=$((CURRENT-1)) \\ + %(autocomplete_var)s=\"complete_zsh\" \\ + %(script_names)s )}") + + for key descr in ${(kv)response}; do + if [[ "$descr" == "_" ]]; then + completions+=("$key") + else + completions_with_descriptions+=("$key":"$descr") + fi + done + + if [ -n "$completions_with_descriptions" ]; then + _describe -V unsorted completions_with_descriptions -U -Q + fi + + if [ -n "$completions" ]; then + compadd -U -V unsorted -Q -a completions + fi + compstate[insert]="automenu" +} + +compdef %(complete_func)s %(script_names)s +''' + +_invalid_ident_char_re = re.compile(r'[^a-zA-Z0-9_]') + + +def get_completion_script(prog_name, complete_var, shell): + cf_name = _invalid_ident_char_re.sub('', prog_name.replace('-', '_')) + script = COMPLETION_SCRIPT_ZSH if shell == 'zsh' else COMPLETION_SCRIPT_BASH + return (script % { + 'complete_func': '_%s_completion' % cf_name, + 'script_names': prog_name, + 'autocomplete_var': complete_var, + }).strip() + ';' + + +def resolve_ctx(cli, prog_name, args): + """ + Parse into a hierarchy of contexts. Contexts are connected through the parent variable. + :param cli: command definition + :param prog_name: the program that is running + :param args: full list of args + :return: the final context/command parsed + """ + ctx = cli.make_context(prog_name, args, resilient_parsing=True) + args = ctx.protected_args + ctx.args + while args: + if isinstance(ctx.command, MultiCommand): + if not ctx.command.chain: + cmd_name, cmd, args = ctx.command.resolve_command(ctx, args) + if cmd is None: + return ctx + ctx = cmd.make_context(cmd_name, args, parent=ctx, + resilient_parsing=True) + args = ctx.protected_args + ctx.args + else: + # Walk chained subcommand contexts saving the last one. + while args: + cmd_name, cmd, args = ctx.command.resolve_command(ctx, args) + if cmd is None: + return ctx + sub_ctx = cmd.make_context(cmd_name, args, parent=ctx, + allow_extra_args=True, + allow_interspersed_args=False, + resilient_parsing=True) + args = sub_ctx.args + ctx = sub_ctx + args = sub_ctx.protected_args + sub_ctx.args + else: + break + return ctx + + +def start_of_option(param_str): + """ + :param param_str: param_str to check + :return: whether or not this is the start of an option declaration (i.e. starts "-" or "--") + """ + return param_str and param_str[:1] == '-' + + +def is_incomplete_option(all_args, cmd_param): + """ + :param all_args: the full original list of args supplied + :param cmd_param: the current command paramter + :return: whether or not the last option declaration (i.e. starts "-" or "--") is incomplete and + corresponds to this cmd_param. In other words whether this cmd_param option can still accept + values + """ + if not isinstance(cmd_param, Option): + return False + if cmd_param.is_flag: + return False + last_option = None + for index, arg_str in enumerate(reversed([arg for arg in all_args if arg != WORDBREAK])): + if index + 1 > cmd_param.nargs: + break + if start_of_option(arg_str): + last_option = arg_str + + return True if last_option and last_option in cmd_param.opts else False + + +def is_incomplete_argument(current_params, cmd_param): + """ + :param current_params: the current params and values for this argument as already entered + :param cmd_param: the current command parameter + :return: whether or not the last argument is incomplete and corresponds to this cmd_param. In + other words whether or not the this cmd_param argument can still accept values + """ + if not isinstance(cmd_param, Argument): + return False + current_param_values = current_params[cmd_param.name] + if current_param_values is None: + return True + if cmd_param.nargs == -1: + return True + if isinstance(current_param_values, abc.Iterable) \ + and cmd_param.nargs > 1 and len(current_param_values) < cmd_param.nargs: + return True + return False + + +def get_user_autocompletions(ctx, args, incomplete, cmd_param): + """ + :param ctx: context associated with the parsed command + :param args: full list of args + :param incomplete: the incomplete text to autocomplete + :param cmd_param: command definition + :return: all the possible user-specified completions for the param + """ + results = [] + if isinstance(cmd_param.type, Choice): + # Choices don't support descriptions. + results = [(c, None) + for c in cmd_param.type.choices if str(c).startswith(incomplete)] + elif cmd_param.autocompletion is not None: + dynamic_completions = cmd_param.autocompletion(ctx=ctx, + args=args, + incomplete=incomplete) + results = [c if isinstance(c, tuple) else (c, None) + for c in dynamic_completions] + return results + + +def get_visible_commands_starting_with(ctx, starts_with): + """ + :param ctx: context associated with the parsed command + :starts_with: string that visible commands must start with. + :return: all visible (not hidden) commands that start with starts_with. + """ + for c in ctx.command.list_commands(ctx): + if c.startswith(starts_with): + command = ctx.command.get_command(ctx, c) + if not command.hidden: + yield command + + +def add_subcommand_completions(ctx, incomplete, completions_out): + # Add subcommand completions. + if isinstance(ctx.command, MultiCommand): + completions_out.extend( + [(c.name, c.get_short_help_str()) for c in get_visible_commands_starting_with(ctx, incomplete)]) + + # Walk up the context list and add any other completion possibilities from chained commands + while ctx.parent is not None: + ctx = ctx.parent + if isinstance(ctx.command, MultiCommand) and ctx.command.chain: + remaining_commands = [c for c in get_visible_commands_starting_with(ctx, incomplete) + if c.name not in ctx.protected_args] + completions_out.extend([(c.name, c.get_short_help_str()) for c in remaining_commands]) + + +def get_choices(cli, prog_name, args, incomplete): + """ + :param cli: command definition + :param prog_name: the program that is running + :param args: full list of args + :param incomplete: the incomplete text to autocomplete + :return: all the possible completions for the incomplete + """ + all_args = copy.deepcopy(args) + + ctx = resolve_ctx(cli, prog_name, args) + if ctx is None: + return [] + + # In newer versions of bash long opts with '='s are partitioned, but it's easier to parse + # without the '=' + if start_of_option(incomplete) and WORDBREAK in incomplete: + partition_incomplete = incomplete.partition(WORDBREAK) + all_args.append(partition_incomplete[0]) + incomplete = partition_incomplete[2] + elif incomplete == WORDBREAK: + incomplete = '' + + completions = [] + if start_of_option(incomplete): + # completions for partial options + for param in ctx.command.params: + if isinstance(param, Option) and not param.hidden: + param_opts = [param_opt for param_opt in param.opts + + param.secondary_opts if param_opt not in all_args or param.multiple] + completions.extend([(o, param.help) for o in param_opts if o.startswith(incomplete)]) + return completions + # completion for option values from user supplied values + for param in ctx.command.params: + if is_incomplete_option(all_args, param): + return get_user_autocompletions(ctx, all_args, incomplete, param) + # completion for argument values from user supplied values + for param in ctx.command.params: + if is_incomplete_argument(ctx.params, param): + return get_user_autocompletions(ctx, all_args, incomplete, param) + + add_subcommand_completions(ctx, incomplete, completions) + # Sort before returning so that proper ordering can be enforced in custom types. + return sorted(completions) + + +def do_complete(cli, prog_name, include_descriptions): + cwords = split_arg_string(os.environ['COMP_WORDS']) + cword = int(os.environ['COMP_CWORD']) + args = cwords[1:cword] + try: + incomplete = cwords[cword] + except IndexError: + incomplete = '' + + for item in get_choices(cli, prog_name, args, incomplete): + echo(item[0]) + if include_descriptions: + # ZSH has trouble dealing with empty array parameters when returned from commands, so use a well defined character '_' to indicate no description is present. + echo(item[1] if item[1] else '_') + + return True + + +def bashcomplete(cli, prog_name, complete_var, complete_instr): + if complete_instr.startswith('source'): + shell = 'zsh' if complete_instr == 'source_zsh' else 'bash' + echo(get_completion_script(prog_name, complete_var, shell)) + return True + elif complete_instr == 'complete' or complete_instr == 'complete_zsh': + return do_complete(cli, prog_name, complete_instr == 'complete_zsh') + return False diff --git a/libs/click/_compat.py b/libs/click/_compat.py new file mode 100644 index 00000000..937e2301 --- /dev/null +++ b/libs/click/_compat.py @@ -0,0 +1,703 @@ +import re +import io +import os +import sys +import codecs +from weakref import WeakKeyDictionary + + +PY2 = sys.version_info[0] == 2 +CYGWIN = sys.platform.startswith('cygwin') +# Determine local App Engine environment, per Google's own suggestion +APP_ENGINE = ('APPENGINE_RUNTIME' in os.environ and + 'Development/' in os.environ['SERVER_SOFTWARE']) +WIN = sys.platform.startswith('win') and not APP_ENGINE +DEFAULT_COLUMNS = 80 + + +_ansi_re = re.compile(r'\033\[((?:\d|;)*)([a-zA-Z])') + + +def get_filesystem_encoding(): + return sys.getfilesystemencoding() or sys.getdefaultencoding() + + +def _make_text_stream(stream, encoding, errors, + force_readable=False, force_writable=False): + if encoding is None: + encoding = get_best_encoding(stream) + if errors is None: + errors = 'replace' + return _NonClosingTextIOWrapper(stream, encoding, errors, + line_buffering=True, + force_readable=force_readable, + force_writable=force_writable) + + +def is_ascii_encoding(encoding): + """Checks if a given encoding is ascii.""" + try: + return codecs.lookup(encoding).name == 'ascii' + except LookupError: + return False + + +def get_best_encoding(stream): + """Returns the default stream encoding if not found.""" + rv = getattr(stream, 'encoding', None) or sys.getdefaultencoding() + if is_ascii_encoding(rv): + return 'utf-8' + return rv + + +class _NonClosingTextIOWrapper(io.TextIOWrapper): + + def __init__(self, stream, encoding, errors, + force_readable=False, force_writable=False, **extra): + self._stream = stream = _FixupStream(stream, force_readable, + force_writable) + io.TextIOWrapper.__init__(self, stream, encoding, errors, **extra) + + # The io module is a place where the Python 3 text behavior + # was forced upon Python 2, so we need to unbreak + # it to look like Python 2. + if PY2: + def write(self, x): + if isinstance(x, str) or is_bytes(x): + try: + self.flush() + except Exception: + pass + return self.buffer.write(str(x)) + return io.TextIOWrapper.write(self, x) + + def writelines(self, lines): + for line in lines: + self.write(line) + + def __del__(self): + try: + self.detach() + except Exception: + pass + + def isatty(self): + # https://bitbucket.org/pypy/pypy/issue/1803 + return self._stream.isatty() + + +class _FixupStream(object): + """The new io interface needs more from streams than streams + traditionally implement. As such, this fix-up code is necessary in + some circumstances. + + The forcing of readable and writable flags are there because some tools + put badly patched objects on sys (one such offender are certain version + of jupyter notebook). + """ + + def __init__(self, stream, force_readable=False, force_writable=False): + self._stream = stream + self._force_readable = force_readable + self._force_writable = force_writable + + def __getattr__(self, name): + return getattr(self._stream, name) + + def read1(self, size): + f = getattr(self._stream, 'read1', None) + if f is not None: + return f(size) + # We only dispatch to readline instead of read in Python 2 as we + # do not want cause problems with the different implementation + # of line buffering. + if PY2: + return self._stream.readline(size) + return self._stream.read(size) + + def readable(self): + if self._force_readable: + return True + x = getattr(self._stream, 'readable', None) + if x is not None: + return x() + try: + self._stream.read(0) + except Exception: + return False + return True + + def writable(self): + if self._force_writable: + return True + x = getattr(self._stream, 'writable', None) + if x is not None: + return x() + try: + self._stream.write('') + except Exception: + try: + self._stream.write(b'') + except Exception: + return False + return True + + def seekable(self): + x = getattr(self._stream, 'seekable', None) + if x is not None: + return x() + try: + self._stream.seek(self._stream.tell()) + except Exception: + return False + return True + + +if PY2: + text_type = unicode + bytes = str + raw_input = raw_input + string_types = (str, unicode) + int_types = (int, long) + iteritems = lambda x: x.iteritems() + range_type = xrange + + def is_bytes(x): + return isinstance(x, (buffer, bytearray)) + + _identifier_re = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]*$') + + # For Windows, we need to force stdout/stdin/stderr to binary if it's + # fetched for that. This obviously is not the most correct way to do + # it as it changes global state. Unfortunately, there does not seem to + # be a clear better way to do it as just reopening the file in binary + # mode does not change anything. + # + # An option would be to do what Python 3 does and to open the file as + # binary only, patch it back to the system, and then use a wrapper + # stream that converts newlines. It's not quite clear what's the + # correct option here. + # + # This code also lives in _winconsole for the fallback to the console + # emulation stream. + # + # There are also Windows environments where the `msvcrt` module is not + # available (which is why we use try-catch instead of the WIN variable + # here), such as the Google App Engine development server on Windows. In + # those cases there is just nothing we can do. + def set_binary_mode(f): + return f + + try: + import msvcrt + except ImportError: + pass + else: + def set_binary_mode(f): + try: + fileno = f.fileno() + except Exception: + pass + else: + msvcrt.setmode(fileno, os.O_BINARY) + return f + + try: + import fcntl + except ImportError: + pass + else: + def set_binary_mode(f): + try: + fileno = f.fileno() + except Exception: + pass + else: + flags = fcntl.fcntl(fileno, fcntl.F_GETFL) + fcntl.fcntl(fileno, fcntl.F_SETFL, flags & ~os.O_NONBLOCK) + return f + + def isidentifier(x): + return _identifier_re.search(x) is not None + + def get_binary_stdin(): + return set_binary_mode(sys.stdin) + + def get_binary_stdout(): + _wrap_std_stream('stdout') + return set_binary_mode(sys.stdout) + + def get_binary_stderr(): + _wrap_std_stream('stderr') + return set_binary_mode(sys.stderr) + + def get_text_stdin(encoding=None, errors=None): + rv = _get_windows_console_stream(sys.stdin, encoding, errors) + if rv is not None: + return rv + return _make_text_stream(sys.stdin, encoding, errors, + force_readable=True) + + def get_text_stdout(encoding=None, errors=None): + _wrap_std_stream('stdout') + rv = _get_windows_console_stream(sys.stdout, encoding, errors) + if rv is not None: + return rv + return _make_text_stream(sys.stdout, encoding, errors, + force_writable=True) + + def get_text_stderr(encoding=None, errors=None): + _wrap_std_stream('stderr') + rv = _get_windows_console_stream(sys.stderr, encoding, errors) + if rv is not None: + return rv + return _make_text_stream(sys.stderr, encoding, errors, + force_writable=True) + + def filename_to_ui(value): + if isinstance(value, bytes): + value = value.decode(get_filesystem_encoding(), 'replace') + return value +else: + import io + text_type = str + raw_input = input + string_types = (str,) + int_types = (int,) + range_type = range + isidentifier = lambda x: x.isidentifier() + iteritems = lambda x: iter(x.items()) + + def is_bytes(x): + return isinstance(x, (bytes, memoryview, bytearray)) + + def _is_binary_reader(stream, default=False): + try: + return isinstance(stream.read(0), bytes) + except Exception: + return default + # This happens in some cases where the stream was already + # closed. In this case, we assume the default. + + def _is_binary_writer(stream, default=False): + try: + stream.write(b'') + except Exception: + try: + stream.write('') + return False + except Exception: + pass + return default + return True + + def _find_binary_reader(stream): + # We need to figure out if the given stream is already binary. + # This can happen because the official docs recommend detaching + # the streams to get binary streams. Some code might do this, so + # we need to deal with this case explicitly. + if _is_binary_reader(stream, False): + return stream + + buf = getattr(stream, 'buffer', None) + + # Same situation here; this time we assume that the buffer is + # actually binary in case it's closed. + if buf is not None and _is_binary_reader(buf, True): + return buf + + def _find_binary_writer(stream): + # We need to figure out if the given stream is already binary. + # This can happen because the official docs recommend detatching + # the streams to get binary streams. Some code might do this, so + # we need to deal with this case explicitly. + if _is_binary_writer(stream, False): + return stream + + buf = getattr(stream, 'buffer', None) + + # Same situation here; this time we assume that the buffer is + # actually binary in case it's closed. + if buf is not None and _is_binary_writer(buf, True): + return buf + + def _stream_is_misconfigured(stream): + """A stream is misconfigured if its encoding is ASCII.""" + # If the stream does not have an encoding set, we assume it's set + # to ASCII. This appears to happen in certain unittest + # environments. It's not quite clear what the correct behavior is + # but this at least will force Click to recover somehow. + return is_ascii_encoding(getattr(stream, 'encoding', None) or 'ascii') + + def _is_compatible_text_stream(stream, encoding, errors): + stream_encoding = getattr(stream, 'encoding', None) + stream_errors = getattr(stream, 'errors', None) + + # Perfect match. + if stream_encoding == encoding and stream_errors == errors: + return True + + # Otherwise, it's only a compatible stream if we did not ask for + # an encoding. + if encoding is None: + return stream_encoding is not None + + return False + + def _force_correct_text_reader(text_reader, encoding, errors, + force_readable=False): + if _is_binary_reader(text_reader, False): + binary_reader = text_reader + else: + # If there is no target encoding set, we need to verify that the + # reader is not actually misconfigured. + if encoding is None and not _stream_is_misconfigured(text_reader): + return text_reader + + if _is_compatible_text_stream(text_reader, encoding, errors): + return text_reader + + # If the reader has no encoding, we try to find the underlying + # binary reader for it. If that fails because the environment is + # misconfigured, we silently go with the same reader because this + # is too common to happen. In that case, mojibake is better than + # exceptions. + binary_reader = _find_binary_reader(text_reader) + if binary_reader is None: + return text_reader + + # At this point, we default the errors to replace instead of strict + # because nobody handles those errors anyways and at this point + # we're so fundamentally fucked that nothing can repair it. + if errors is None: + errors = 'replace' + return _make_text_stream(binary_reader, encoding, errors, + force_readable=force_readable) + + def _force_correct_text_writer(text_writer, encoding, errors, + force_writable=False): + if _is_binary_writer(text_writer, False): + binary_writer = text_writer + else: + # If there is no target encoding set, we need to verify that the + # writer is not actually misconfigured. + if encoding is None and not _stream_is_misconfigured(text_writer): + return text_writer + + if _is_compatible_text_stream(text_writer, encoding, errors): + return text_writer + + # If the writer has no encoding, we try to find the underlying + # binary writer for it. If that fails because the environment is + # misconfigured, we silently go with the same writer because this + # is too common to happen. In that case, mojibake is better than + # exceptions. + binary_writer = _find_binary_writer(text_writer) + if binary_writer is None: + return text_writer + + # At this point, we default the errors to replace instead of strict + # because nobody handles those errors anyways and at this point + # we're so fundamentally fucked that nothing can repair it. + if errors is None: + errors = 'replace' + return _make_text_stream(binary_writer, encoding, errors, + force_writable=force_writable) + + def get_binary_stdin(): + reader = _find_binary_reader(sys.stdin) + if reader is None: + raise RuntimeError('Was not able to determine binary ' + 'stream for sys.stdin.') + return reader + + def get_binary_stdout(): + writer = _find_binary_writer(sys.stdout) + if writer is None: + raise RuntimeError('Was not able to determine binary ' + 'stream for sys.stdout.') + return writer + + def get_binary_stderr(): + writer = _find_binary_writer(sys.stderr) + if writer is None: + raise RuntimeError('Was not able to determine binary ' + 'stream for sys.stderr.') + return writer + + def get_text_stdin(encoding=None, errors=None): + rv = _get_windows_console_stream(sys.stdin, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_reader(sys.stdin, encoding, errors, + force_readable=True) + + def get_text_stdout(encoding=None, errors=None): + rv = _get_windows_console_stream(sys.stdout, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_writer(sys.stdout, encoding, errors, + force_writable=True) + + def get_text_stderr(encoding=None, errors=None): + rv = _get_windows_console_stream(sys.stderr, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_writer(sys.stderr, encoding, errors, + force_writable=True) + + def filename_to_ui(value): + if isinstance(value, bytes): + value = value.decode(get_filesystem_encoding(), 'replace') + else: + value = value.encode('utf-8', 'surrogateescape') \ + .decode('utf-8', 'replace') + return value + + +def get_streerror(e, default=None): + if hasattr(e, 'strerror'): + msg = e.strerror + else: + if default is not None: + msg = default + else: + msg = str(e) + if isinstance(msg, bytes): + msg = msg.decode('utf-8', 'replace') + return msg + + +def open_stream(filename, mode='r', encoding=None, errors='strict', + atomic=False): + # Standard streams first. These are simple because they don't need + # special handling for the atomic flag. It's entirely ignored. + if filename == '-': + if any(m in mode for m in ['w', 'a', 'x']): + if 'b' in mode: + return get_binary_stdout(), False + return get_text_stdout(encoding=encoding, errors=errors), False + if 'b' in mode: + return get_binary_stdin(), False + return get_text_stdin(encoding=encoding, errors=errors), False + + # Non-atomic writes directly go out through the regular open functions. + if not atomic: + if encoding is None: + return open(filename, mode), True + return io.open(filename, mode, encoding=encoding, errors=errors), True + + # Some usability stuff for atomic writes + if 'a' in mode: + raise ValueError( + 'Appending to an existing file is not supported, because that ' + 'would involve an expensive `copy`-operation to a temporary ' + 'file. Open the file in normal `w`-mode and copy explicitly ' + 'if that\'s what you\'re after.' + ) + if 'x' in mode: + raise ValueError('Use the `overwrite`-parameter instead.') + if 'w' not in mode: + raise ValueError('Atomic writes only make sense with `w`-mode.') + + # Atomic writes are more complicated. They work by opening a file + # as a proxy in the same folder and then using the fdopen + # functionality to wrap it in a Python file. Then we wrap it in an + # atomic file that moves the file over on close. + import tempfile + fd, tmp_filename = tempfile.mkstemp(dir=os.path.dirname(filename), + prefix='.__atomic-write') + + if encoding is not None: + f = io.open(fd, mode, encoding=encoding, errors=errors) + else: + f = os.fdopen(fd, mode) + + return _AtomicFile(f, tmp_filename, os.path.realpath(filename)), True + + +# Used in a destructor call, needs extra protection from interpreter cleanup. +if hasattr(os, 'replace'): + _replace = os.replace + _can_replace = True +else: + _replace = os.rename + _can_replace = not WIN + + +class _AtomicFile(object): + + def __init__(self, f, tmp_filename, real_filename): + self._f = f + self._tmp_filename = tmp_filename + self._real_filename = real_filename + self.closed = False + + @property + def name(self): + return self._real_filename + + def close(self, delete=False): + if self.closed: + return + self._f.close() + if not _can_replace: + try: + os.remove(self._real_filename) + except OSError: + pass + _replace(self._tmp_filename, self._real_filename) + self.closed = True + + def __getattr__(self, name): + return getattr(self._f, name) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, tb): + self.close(delete=exc_type is not None) + + def __repr__(self): + return repr(self._f) + + +auto_wrap_for_ansi = None +colorama = None +get_winterm_size = None + + +def strip_ansi(value): + return _ansi_re.sub('', value) + + +def should_strip_ansi(stream=None, color=None): + if color is None: + if stream is None: + stream = sys.stdin + return not isatty(stream) + return not color + + +# If we're on Windows, we provide transparent integration through +# colorama. This will make ANSI colors through the echo function +# work automatically. +if WIN: + # Windows has a smaller terminal + DEFAULT_COLUMNS = 79 + + from ._winconsole import _get_windows_console_stream, _wrap_std_stream + + def _get_argv_encoding(): + import locale + return locale.getpreferredencoding() + + if PY2: + def raw_input(prompt=''): + sys.stderr.flush() + if prompt: + stdout = _default_text_stdout() + stdout.write(prompt) + stdin = _default_text_stdin() + return stdin.readline().rstrip('\r\n') + + try: + import colorama + except ImportError: + pass + else: + _ansi_stream_wrappers = WeakKeyDictionary() + + def auto_wrap_for_ansi(stream, color=None): + """This function wraps a stream so that calls through colorama + are issued to the win32 console API to recolor on demand. It + also ensures to reset the colors if a write call is interrupted + to not destroy the console afterwards. + """ + try: + cached = _ansi_stream_wrappers.get(stream) + except Exception: + cached = None + if cached is not None: + return cached + strip = should_strip_ansi(stream, color) + ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip) + rv = ansi_wrapper.stream + _write = rv.write + + def _safe_write(s): + try: + return _write(s) + except: + ansi_wrapper.reset_all() + raise + + rv.write = _safe_write + try: + _ansi_stream_wrappers[stream] = rv + except Exception: + pass + return rv + + def get_winterm_size(): + win = colorama.win32.GetConsoleScreenBufferInfo( + colorama.win32.STDOUT).srWindow + return win.Right - win.Left, win.Bottom - win.Top +else: + def _get_argv_encoding(): + return getattr(sys.stdin, 'encoding', None) or get_filesystem_encoding() + + _get_windows_console_stream = lambda *x: None + _wrap_std_stream = lambda *x: None + + +def term_len(x): + return len(strip_ansi(x)) + + +def isatty(stream): + try: + return stream.isatty() + except Exception: + return False + + +def _make_cached_stream_func(src_func, wrapper_func): + cache = WeakKeyDictionary() + def func(): + stream = src_func() + try: + rv = cache.get(stream) + except Exception: + rv = None + if rv is not None: + return rv + rv = wrapper_func() + try: + stream = src_func() # In case wrapper_func() modified the stream + cache[stream] = rv + except Exception: + pass + return rv + return func + + +_default_text_stdin = _make_cached_stream_func( + lambda: sys.stdin, get_text_stdin) +_default_text_stdout = _make_cached_stream_func( + lambda: sys.stdout, get_text_stdout) +_default_text_stderr = _make_cached_stream_func( + lambda: sys.stderr, get_text_stderr) + + +binary_streams = { + 'stdin': get_binary_stdin, + 'stdout': get_binary_stdout, + 'stderr': get_binary_stderr, +} + +text_streams = { + 'stdin': get_text_stdin, + 'stdout': get_text_stdout, + 'stderr': get_text_stderr, +} diff --git a/libs/click/_termui_impl.py b/libs/click/_termui_impl.py new file mode 100644 index 00000000..00a8e5ef --- /dev/null +++ b/libs/click/_termui_impl.py @@ -0,0 +1,621 @@ +# -*- coding: utf-8 -*- +""" +click._termui_impl +~~~~~~~~~~~~~~~~~~ + +This module contains implementations for the termui module. To keep the +import time of Click down, some infrequently used functionality is +placed in this module and only imported as needed. + +:copyright: © 2014 by the Pallets team. +:license: BSD, see LICENSE.rst for more details. +""" + +import os +import sys +import time +import math +import contextlib +from ._compat import _default_text_stdout, range_type, PY2, isatty, \ + open_stream, strip_ansi, term_len, get_best_encoding, WIN, int_types, \ + CYGWIN +from .utils import echo +from .exceptions import ClickException + + +if os.name == 'nt': + BEFORE_BAR = '\r' + AFTER_BAR = '\n' +else: + BEFORE_BAR = '\r\033[?25l' + AFTER_BAR = '\033[?25h\n' + + +def _length_hint(obj): + """Returns the length hint of an object.""" + try: + return len(obj) + except (AttributeError, TypeError): + try: + get_hint = type(obj).__length_hint__ + except AttributeError: + return None + try: + hint = get_hint(obj) + except TypeError: + return None + if hint is NotImplemented or \ + not isinstance(hint, int_types) or \ + hint < 0: + return None + return hint + + +class ProgressBar(object): + + def __init__(self, iterable, length=None, fill_char='#', empty_char=' ', + bar_template='%(bar)s', info_sep=' ', show_eta=True, + show_percent=None, show_pos=False, item_show_func=None, + label=None, file=None, color=None, width=30): + self.fill_char = fill_char + self.empty_char = empty_char + self.bar_template = bar_template + self.info_sep = info_sep + self.show_eta = show_eta + self.show_percent = show_percent + self.show_pos = show_pos + self.item_show_func = item_show_func + self.label = label or '' + if file is None: + file = _default_text_stdout() + self.file = file + self.color = color + self.width = width + self.autowidth = width == 0 + + if length is None: + length = _length_hint(iterable) + if iterable is None: + if length is None: + raise TypeError('iterable or length is required') + iterable = range_type(length) + self.iter = iter(iterable) + self.length = length + self.length_known = length is not None + self.pos = 0 + self.avg = [] + self.start = self.last_eta = time.time() + self.eta_known = False + self.finished = False + self.max_width = None + self.entered = False + self.current_item = None + self.is_hidden = not isatty(self.file) + self._last_line = None + self.short_limit = 0.5 + + def __enter__(self): + self.entered = True + self.render_progress() + return self + + def __exit__(self, exc_type, exc_value, tb): + self.render_finish() + + def __iter__(self): + if not self.entered: + raise RuntimeError('You need to use progress bars in a with block.') + self.render_progress() + return self.generator() + + def is_fast(self): + return time.time() - self.start <= self.short_limit + + def render_finish(self): + if self.is_hidden or self.is_fast(): + return + self.file.write(AFTER_BAR) + self.file.flush() + + @property + def pct(self): + if self.finished: + return 1.0 + return min(self.pos / (float(self.length) or 1), 1.0) + + @property + def time_per_iteration(self): + if not self.avg: + return 0.0 + return sum(self.avg) / float(len(self.avg)) + + @property + def eta(self): + if self.length_known and not self.finished: + return self.time_per_iteration * (self.length - self.pos) + return 0.0 + + def format_eta(self): + if self.eta_known: + t = int(self.eta) + seconds = t % 60 + t //= 60 + minutes = t % 60 + t //= 60 + hours = t % 24 + t //= 24 + if t > 0: + days = t + return '%dd %02d:%02d:%02d' % (days, hours, minutes, seconds) + else: + return '%02d:%02d:%02d' % (hours, minutes, seconds) + return '' + + def format_pos(self): + pos = str(self.pos) + if self.length_known: + pos += '/%s' % self.length + return pos + + def format_pct(self): + return ('% 4d%%' % int(self.pct * 100))[1:] + + def format_bar(self): + if self.length_known: + bar_length = int(self.pct * self.width) + bar = self.fill_char * bar_length + bar += self.empty_char * (self.width - bar_length) + elif self.finished: + bar = self.fill_char * self.width + else: + bar = list(self.empty_char * (self.width or 1)) + if self.time_per_iteration != 0: + bar[int((math.cos(self.pos * self.time_per_iteration) + / 2.0 + 0.5) * self.width)] = self.fill_char + bar = ''.join(bar) + return bar + + def format_progress_line(self): + show_percent = self.show_percent + + info_bits = [] + if self.length_known and show_percent is None: + show_percent = not self.show_pos + + if self.show_pos: + info_bits.append(self.format_pos()) + if show_percent: + info_bits.append(self.format_pct()) + if self.show_eta and self.eta_known and not self.finished: + info_bits.append(self.format_eta()) + if self.item_show_func is not None: + item_info = self.item_show_func(self.current_item) + if item_info is not None: + info_bits.append(item_info) + + return (self.bar_template % { + 'label': self.label, + 'bar': self.format_bar(), + 'info': self.info_sep.join(info_bits) + }).rstrip() + + def render_progress(self): + from .termui import get_terminal_size + + if self.is_hidden: + return + + buf = [] + # Update width in case the terminal has been resized + if self.autowidth: + old_width = self.width + self.width = 0 + clutter_length = term_len(self.format_progress_line()) + new_width = max(0, get_terminal_size()[0] - clutter_length) + if new_width < old_width: + buf.append(BEFORE_BAR) + buf.append(' ' * self.max_width) + self.max_width = new_width + self.width = new_width + + clear_width = self.width + if self.max_width is not None: + clear_width = self.max_width + + buf.append(BEFORE_BAR) + line = self.format_progress_line() + line_len = term_len(line) + if self.max_width is None or self.max_width < line_len: + self.max_width = line_len + + buf.append(line) + buf.append(' ' * (clear_width - line_len)) + line = ''.join(buf) + # Render the line only if it changed. + + if line != self._last_line and not self.is_fast(): + self._last_line = line + echo(line, file=self.file, color=self.color, nl=False) + self.file.flush() + + def make_step(self, n_steps): + self.pos += n_steps + if self.length_known and self.pos >= self.length: + self.finished = True + + if (time.time() - self.last_eta) < 1.0: + return + + self.last_eta = time.time() + + # self.avg is a rolling list of length <= 7 of steps where steps are + # defined as time elapsed divided by the total progress through + # self.length. + if self.pos: + step = (time.time() - self.start) / self.pos + else: + step = time.time() - self.start + + self.avg = self.avg[-6:] + [step] + + self.eta_known = self.length_known + + def update(self, n_steps): + self.make_step(n_steps) + self.render_progress() + + def finish(self): + self.eta_known = 0 + self.current_item = None + self.finished = True + + def generator(self): + """ + Returns a generator which yields the items added to the bar during + construction, and updates the progress bar *after* the yielded block + returns. + """ + if not self.entered: + raise RuntimeError('You need to use progress bars in a with block.') + + if self.is_hidden: + for rv in self.iter: + yield rv + else: + for rv in self.iter: + self.current_item = rv + yield rv + self.update(1) + self.finish() + self.render_progress() + + +def pager(generator, color=None): + """Decide what method to use for paging through text.""" + stdout = _default_text_stdout() + if not isatty(sys.stdin) or not isatty(stdout): + return _nullpager(stdout, generator, color) + pager_cmd = (os.environ.get('PAGER', None) or '').strip() + if pager_cmd: + if WIN: + return _tempfilepager(generator, pager_cmd, color) + return _pipepager(generator, pager_cmd, color) + if os.environ.get('TERM') in ('dumb', 'emacs'): + return _nullpager(stdout, generator, color) + if WIN or sys.platform.startswith('os2'): + return _tempfilepager(generator, 'more <', color) + if hasattr(os, 'system') and os.system('(less) 2>/dev/null') == 0: + return _pipepager(generator, 'less', color) + + import tempfile + fd, filename = tempfile.mkstemp() + os.close(fd) + try: + if hasattr(os, 'system') and os.system('more "%s"' % filename) == 0: + return _pipepager(generator, 'more', color) + return _nullpager(stdout, generator, color) + finally: + os.unlink(filename) + + +def _pipepager(generator, cmd, color): + """Page through text by feeding it to another program. Invoking a + pager through this might support colors. + """ + import subprocess + env = dict(os.environ) + + # If we're piping to less we might support colors under the + # condition that + cmd_detail = cmd.rsplit('/', 1)[-1].split() + if color is None and cmd_detail[0] == 'less': + less_flags = os.environ.get('LESS', '') + ' '.join(cmd_detail[1:]) + if not less_flags: + env['LESS'] = '-R' + color = True + elif 'r' in less_flags or 'R' in less_flags: + color = True + + c = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, + env=env) + encoding = get_best_encoding(c.stdin) + try: + for text in generator: + if not color: + text = strip_ansi(text) + + c.stdin.write(text.encode(encoding, 'replace')) + except (IOError, KeyboardInterrupt): + pass + else: + c.stdin.close() + + # Less doesn't respect ^C, but catches it for its own UI purposes (aborting + # search or other commands inside less). + # + # That means when the user hits ^C, the parent process (click) terminates, + # but less is still alive, paging the output and messing up the terminal. + # + # If the user wants to make the pager exit on ^C, they should set + # `LESS='-K'`. It's not our decision to make. + while True: + try: + c.wait() + except KeyboardInterrupt: + pass + else: + break + + +def _tempfilepager(generator, cmd, color): + """Page through text by invoking a program on a temporary file.""" + import tempfile + filename = tempfile.mktemp() + # TODO: This never terminates if the passed generator never terminates. + text = "".join(generator) + if not color: + text = strip_ansi(text) + encoding = get_best_encoding(sys.stdout) + with open_stream(filename, 'wb')[0] as f: + f.write(text.encode(encoding)) + try: + os.system(cmd + ' "' + filename + '"') + finally: + os.unlink(filename) + + +def _nullpager(stream, generator, color): + """Simply print unformatted text. This is the ultimate fallback.""" + for text in generator: + if not color: + text = strip_ansi(text) + stream.write(text) + + +class Editor(object): + + def __init__(self, editor=None, env=None, require_save=True, + extension='.txt'): + self.editor = editor + self.env = env + self.require_save = require_save + self.extension = extension + + def get_editor(self): + if self.editor is not None: + return self.editor + for key in 'VISUAL', 'EDITOR': + rv = os.environ.get(key) + if rv: + return rv + if WIN: + return 'notepad' + for editor in 'vim', 'nano': + if os.system('which %s >/dev/null 2>&1' % editor) == 0: + return editor + return 'vi' + + def edit_file(self, filename): + import subprocess + editor = self.get_editor() + if self.env: + environ = os.environ.copy() + environ.update(self.env) + else: + environ = None + try: + c = subprocess.Popen('%s "%s"' % (editor, filename), + env=environ, shell=True) + exit_code = c.wait() + if exit_code != 0: + raise ClickException('%s: Editing failed!' % editor) + except OSError as e: + raise ClickException('%s: Editing failed: %s' % (editor, e)) + + def edit(self, text): + import tempfile + + text = text or '' + if text and not text.endswith('\n'): + text += '\n' + + fd, name = tempfile.mkstemp(prefix='editor-', suffix=self.extension) + try: + if WIN: + encoding = 'utf-8-sig' + text = text.replace('\n', '\r\n') + else: + encoding = 'utf-8' + text = text.encode(encoding) + + f = os.fdopen(fd, 'wb') + f.write(text) + f.close() + timestamp = os.path.getmtime(name) + + self.edit_file(name) + + if self.require_save \ + and os.path.getmtime(name) == timestamp: + return None + + f = open(name, 'rb') + try: + rv = f.read() + finally: + f.close() + return rv.decode('utf-8-sig').replace('\r\n', '\n') + finally: + os.unlink(name) + + +def open_url(url, wait=False, locate=False): + import subprocess + + def _unquote_file(url): + try: + import urllib + except ImportError: + import urllib + if url.startswith('file://'): + url = urllib.unquote(url[7:]) + return url + + if sys.platform == 'darwin': + args = ['open'] + if wait: + args.append('-W') + if locate: + args.append('-R') + args.append(_unquote_file(url)) + null = open('/dev/null', 'w') + try: + return subprocess.Popen(args, stderr=null).wait() + finally: + null.close() + elif WIN: + if locate: + url = _unquote_file(url) + args = 'explorer /select,"%s"' % _unquote_file( + url.replace('"', '')) + else: + args = 'start %s "" "%s"' % ( + wait and '/WAIT' or '', url.replace('"', '')) + return os.system(args) + elif CYGWIN: + if locate: + url = _unquote_file(url) + args = 'cygstart "%s"' % (os.path.dirname(url).replace('"', '')) + else: + args = 'cygstart %s "%s"' % ( + wait and '-w' or '', url.replace('"', '')) + return os.system(args) + + try: + if locate: + url = os.path.dirname(_unquote_file(url)) or '.' + else: + url = _unquote_file(url) + c = subprocess.Popen(['xdg-open', url]) + if wait: + return c.wait() + return 0 + except OSError: + if url.startswith(('http://', 'https://')) and not locate and not wait: + import webbrowser + webbrowser.open(url) + return 0 + return 1 + + +def _translate_ch_to_exc(ch): + if ch == u'\x03': + raise KeyboardInterrupt() + if ch == u'\x04' and not WIN: # Unix-like, Ctrl+D + raise EOFError() + if ch == u'\x1a' and WIN: # Windows, Ctrl+Z + raise EOFError() + + +if WIN: + import msvcrt + + @contextlib.contextmanager + def raw_terminal(): + yield + + def getchar(echo): + # The function `getch` will return a bytes object corresponding to + # the pressed character. Since Windows 10 build 1803, it will also + # return \x00 when called a second time after pressing a regular key. + # + # `getwch` does not share this probably-bugged behavior. Moreover, it + # returns a Unicode object by default, which is what we want. + # + # Either of these functions will return \x00 or \xe0 to indicate + # a special key, and you need to call the same function again to get + # the "rest" of the code. The fun part is that \u00e0 is + # "latin small letter a with grave", so if you type that on a French + # keyboard, you _also_ get a \xe0. + # E.g., consider the Up arrow. This returns \xe0 and then \x48. The + # resulting Unicode string reads as "a with grave" + "capital H". + # This is indistinguishable from when the user actually types + # "a with grave" and then "capital H". + # + # When \xe0 is returned, we assume it's part of a special-key sequence + # and call `getwch` again, but that means that when the user types + # the \u00e0 character, `getchar` doesn't return until a second + # character is typed. + # The alternative is returning immediately, but that would mess up + # cross-platform handling of arrow keys and others that start with + # \xe0. Another option is using `getch`, but then we can't reliably + # read non-ASCII characters, because return values of `getch` are + # limited to the current 8-bit codepage. + # + # Anyway, Click doesn't claim to do this Right(tm), and using `getwch` + # is doing the right thing in more situations than with `getch`. + if echo: + func = msvcrt.getwche + else: + func = msvcrt.getwch + + rv = func() + if rv in (u'\x00', u'\xe0'): + # \x00 and \xe0 are control characters that indicate special key, + # see above. + rv += func() + _translate_ch_to_exc(rv) + return rv +else: + import tty + import termios + + @contextlib.contextmanager + def raw_terminal(): + if not isatty(sys.stdin): + f = open('/dev/tty') + fd = f.fileno() + else: + fd = sys.stdin.fileno() + f = None + try: + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(fd) + yield fd + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + sys.stdout.flush() + if f is not None: + f.close() + except termios.error: + pass + + def getchar(echo): + with raw_terminal() as fd: + ch = os.read(fd, 32) + ch = ch.decode(get_best_encoding(sys.stdin), 'replace') + if echo and isatty(sys.stdout): + sys.stdout.write(ch) + _translate_ch_to_exc(ch) + return ch diff --git a/libs/click/_textwrap.py b/libs/click/_textwrap.py new file mode 100644 index 00000000..7e776031 --- /dev/null +++ b/libs/click/_textwrap.py @@ -0,0 +1,38 @@ +import textwrap +from contextlib import contextmanager + + +class TextWrapper(textwrap.TextWrapper): + + def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): + space_left = max(width - cur_len, 1) + + if self.break_long_words: + last = reversed_chunks[-1] + cut = last[:space_left] + res = last[space_left:] + cur_line.append(cut) + reversed_chunks[-1] = res + elif not cur_line: + cur_line.append(reversed_chunks.pop()) + + @contextmanager + def extra_indent(self, indent): + old_initial_indent = self.initial_indent + old_subsequent_indent = self.subsequent_indent + self.initial_indent += indent + self.subsequent_indent += indent + try: + yield + finally: + self.initial_indent = old_initial_indent + self.subsequent_indent = old_subsequent_indent + + def indent_only(self, text): + rv = [] + for idx, line in enumerate(text.splitlines()): + indent = self.initial_indent + if idx > 0: + indent = self.subsequent_indent + rv.append(indent + line) + return '\n'.join(rv) diff --git a/libs/click/_unicodefun.py b/libs/click/_unicodefun.py new file mode 100644 index 00000000..620edff3 --- /dev/null +++ b/libs/click/_unicodefun.py @@ -0,0 +1,125 @@ +import os +import sys +import codecs + +from ._compat import PY2 + + +# If someone wants to vendor click, we want to ensure the +# correct package is discovered. Ideally we could use a +# relative import here but unfortunately Python does not +# support that. +click = sys.modules[__name__.rsplit('.', 1)[0]] + + +def _find_unicode_literals_frame(): + import __future__ + if not hasattr(sys, '_getframe'): # not all Python implementations have it + return 0 + frm = sys._getframe(1) + idx = 1 + while frm is not None: + if frm.f_globals.get('__name__', '').startswith('click.'): + frm = frm.f_back + idx += 1 + elif frm.f_code.co_flags & __future__.unicode_literals.compiler_flag: + return idx + else: + break + return 0 + + +def _check_for_unicode_literals(): + if not __debug__: + return + if not PY2 or click.disable_unicode_literals_warning: + return + bad_frame = _find_unicode_literals_frame() + if bad_frame <= 0: + return + from warnings import warn + warn(Warning('Click detected the use of the unicode_literals ' + '__future__ import. This is heavily discouraged ' + 'because it can introduce subtle bugs in your ' + 'code. You should instead use explicit u"" literals ' + 'for your unicode strings. For more information see ' + 'https://click.palletsprojects.com/python3/'), + stacklevel=bad_frame) + + +def _verify_python3_env(): + """Ensures that the environment is good for unicode on Python 3.""" + if PY2: + return + try: + import locale + fs_enc = codecs.lookup(locale.getpreferredencoding()).name + except Exception: + fs_enc = 'ascii' + if fs_enc != 'ascii': + return + + extra = '' + if os.name == 'posix': + import subprocess + try: + rv = subprocess.Popen(['locale', '-a'], stdout=subprocess.PIPE, + stderr=subprocess.PIPE).communicate()[0] + except OSError: + rv = b'' + good_locales = set() + has_c_utf8 = False + + # Make sure we're operating on text here. + if isinstance(rv, bytes): + rv = rv.decode('ascii', 'replace') + + for line in rv.splitlines(): + locale = line.strip() + if locale.lower().endswith(('.utf-8', '.utf8')): + good_locales.add(locale) + if locale.lower() in ('c.utf8', 'c.utf-8'): + has_c_utf8 = True + + extra += '\n\n' + if not good_locales: + extra += ( + 'Additional information: on this system no suitable UTF-8\n' + 'locales were discovered. This most likely requires resolving\n' + 'by reconfiguring the locale system.' + ) + elif has_c_utf8: + extra += ( + 'This system supports the C.UTF-8 locale which is recommended.\n' + 'You might be able to resolve your issue by exporting the\n' + 'following environment variables:\n\n' + ' export LC_ALL=C.UTF-8\n' + ' export LANG=C.UTF-8' + ) + else: + extra += ( + 'This system lists a couple of UTF-8 supporting locales that\n' + 'you can pick from. The following suitable locales were\n' + 'discovered: %s' + ) % ', '.join(sorted(good_locales)) + + bad_locale = None + for locale in os.environ.get('LC_ALL'), os.environ.get('LANG'): + if locale and locale.lower().endswith(('.utf-8', '.utf8')): + bad_locale = locale + if locale is not None: + break + if bad_locale is not None: + extra += ( + '\n\nClick discovered that you exported a UTF-8 locale\n' + 'but the locale system could not pick up from it because\n' + 'it does not exist. The exported locale is "%s" but it\n' + 'is not supported' + ) % bad_locale + + raise RuntimeError( + 'Click will abort further execution because Python 3 was' + ' configured to use ASCII as encoding for the environment.' + ' Consult https://click.palletsprojects.com/en/7.x/python3/ for' + ' mitigation steps.' + extra + ) diff --git a/libs/click/_winconsole.py b/libs/click/_winconsole.py new file mode 100644 index 00000000..bbb080dd --- /dev/null +++ b/libs/click/_winconsole.py @@ -0,0 +1,307 @@ +# -*- coding: utf-8 -*- +# This module is based on the excellent work by Adam Bartoš who +# provided a lot of what went into the implementation here in +# the discussion to issue1602 in the Python bug tracker. +# +# There are some general differences in regards to how this works +# compared to the original patches as we do not need to patch +# the entire interpreter but just work in our little world of +# echo and prmopt. + +import io +import os +import sys +import zlib +import time +import ctypes +import msvcrt +from ._compat import _NonClosingTextIOWrapper, text_type, PY2 +from ctypes import byref, POINTER, c_int, c_char, c_char_p, \ + c_void_p, py_object, c_ssize_t, c_ulong, windll, WINFUNCTYPE +try: + from ctypes import pythonapi + PyObject_GetBuffer = pythonapi.PyObject_GetBuffer + PyBuffer_Release = pythonapi.PyBuffer_Release +except ImportError: + pythonapi = None +from ctypes.wintypes import LPWSTR, LPCWSTR + + +c_ssize_p = POINTER(c_ssize_t) + +kernel32 = windll.kernel32 +GetStdHandle = kernel32.GetStdHandle +ReadConsoleW = kernel32.ReadConsoleW +WriteConsoleW = kernel32.WriteConsoleW +GetLastError = kernel32.GetLastError +GetCommandLineW = WINFUNCTYPE(LPWSTR)( + ('GetCommandLineW', windll.kernel32)) +CommandLineToArgvW = WINFUNCTYPE( + POINTER(LPWSTR), LPCWSTR, POINTER(c_int))( + ('CommandLineToArgvW', windll.shell32)) + + +STDIN_HANDLE = GetStdHandle(-10) +STDOUT_HANDLE = GetStdHandle(-11) +STDERR_HANDLE = GetStdHandle(-12) + + +PyBUF_SIMPLE = 0 +PyBUF_WRITABLE = 1 + +ERROR_SUCCESS = 0 +ERROR_NOT_ENOUGH_MEMORY = 8 +ERROR_OPERATION_ABORTED = 995 + +STDIN_FILENO = 0 +STDOUT_FILENO = 1 +STDERR_FILENO = 2 + +EOF = b'\x1a' +MAX_BYTES_WRITTEN = 32767 + + +class Py_buffer(ctypes.Structure): + _fields_ = [ + ('buf', c_void_p), + ('obj', py_object), + ('len', c_ssize_t), + ('itemsize', c_ssize_t), + ('readonly', c_int), + ('ndim', c_int), + ('format', c_char_p), + ('shape', c_ssize_p), + ('strides', c_ssize_p), + ('suboffsets', c_ssize_p), + ('internal', c_void_p) + ] + + if PY2: + _fields_.insert(-1, ('smalltable', c_ssize_t * 2)) + + +# On PyPy we cannot get buffers so our ability to operate here is +# serverly limited. +if pythonapi is None: + get_buffer = None +else: + def get_buffer(obj, writable=False): + buf = Py_buffer() + flags = PyBUF_WRITABLE if writable else PyBUF_SIMPLE + PyObject_GetBuffer(py_object(obj), byref(buf), flags) + try: + buffer_type = c_char * buf.len + return buffer_type.from_address(buf.buf) + finally: + PyBuffer_Release(byref(buf)) + + +class _WindowsConsoleRawIOBase(io.RawIOBase): + + def __init__(self, handle): + self.handle = handle + + def isatty(self): + io.RawIOBase.isatty(self) + return True + + +class _WindowsConsoleReader(_WindowsConsoleRawIOBase): + + def readable(self): + return True + + def readinto(self, b): + bytes_to_be_read = len(b) + if not bytes_to_be_read: + return 0 + elif bytes_to_be_read % 2: + raise ValueError('cannot read odd number of bytes from ' + 'UTF-16-LE encoded console') + + buffer = get_buffer(b, writable=True) + code_units_to_be_read = bytes_to_be_read // 2 + code_units_read = c_ulong() + + rv = ReadConsoleW(self.handle, buffer, code_units_to_be_read, + byref(code_units_read), None) + if GetLastError() == ERROR_OPERATION_ABORTED: + # wait for KeyboardInterrupt + time.sleep(0.1) + if not rv: + raise OSError('Windows error: %s' % GetLastError()) + + if buffer[0] == EOF: + return 0 + return 2 * code_units_read.value + + +class _WindowsConsoleWriter(_WindowsConsoleRawIOBase): + + def writable(self): + return True + + @staticmethod + def _get_error_message(errno): + if errno == ERROR_SUCCESS: + return 'ERROR_SUCCESS' + elif errno == ERROR_NOT_ENOUGH_MEMORY: + return 'ERROR_NOT_ENOUGH_MEMORY' + return 'Windows error %s' % errno + + def write(self, b): + bytes_to_be_written = len(b) + buf = get_buffer(b) + code_units_to_be_written = min(bytes_to_be_written, + MAX_BYTES_WRITTEN) // 2 + code_units_written = c_ulong() + + WriteConsoleW(self.handle, buf, code_units_to_be_written, + byref(code_units_written), None) + bytes_written = 2 * code_units_written.value + + if bytes_written == 0 and bytes_to_be_written > 0: + raise OSError(self._get_error_message(GetLastError())) + return bytes_written + + +class ConsoleStream(object): + + def __init__(self, text_stream, byte_stream): + self._text_stream = text_stream + self.buffer = byte_stream + + @property + def name(self): + return self.buffer.name + + def write(self, x): + if isinstance(x, text_type): + return self._text_stream.write(x) + try: + self.flush() + except Exception: + pass + return self.buffer.write(x) + + def writelines(self, lines): + for line in lines: + self.write(line) + + def __getattr__(self, name): + return getattr(self._text_stream, name) + + def isatty(self): + return self.buffer.isatty() + + def __repr__(self): + return '' % ( + self.name, + self.encoding, + ) + + +class WindowsChunkedWriter(object): + """ + Wraps a stream (such as stdout), acting as a transparent proxy for all + attribute access apart from method 'write()' which we wrap to write in + limited chunks due to a Windows limitation on binary console streams. + """ + def __init__(self, wrapped): + # double-underscore everything to prevent clashes with names of + # attributes on the wrapped stream object. + self.__wrapped = wrapped + + def __getattr__(self, name): + return getattr(self.__wrapped, name) + + def write(self, text): + total_to_write = len(text) + written = 0 + + while written < total_to_write: + to_write = min(total_to_write - written, MAX_BYTES_WRITTEN) + self.__wrapped.write(text[written:written+to_write]) + written += to_write + + +_wrapped_std_streams = set() + + +def _wrap_std_stream(name): + # Python 2 & Windows 7 and below + if PY2 and sys.getwindowsversion()[:2] <= (6, 1) and name not in _wrapped_std_streams: + setattr(sys, name, WindowsChunkedWriter(getattr(sys, name))) + _wrapped_std_streams.add(name) + + +def _get_text_stdin(buffer_stream): + text_stream = _NonClosingTextIOWrapper( + io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)), + 'utf-16-le', 'strict', line_buffering=True) + return ConsoleStream(text_stream, buffer_stream) + + +def _get_text_stdout(buffer_stream): + text_stream = _NonClosingTextIOWrapper( + io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)), + 'utf-16-le', 'strict', line_buffering=True) + return ConsoleStream(text_stream, buffer_stream) + + +def _get_text_stderr(buffer_stream): + text_stream = _NonClosingTextIOWrapper( + io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)), + 'utf-16-le', 'strict', line_buffering=True) + return ConsoleStream(text_stream, buffer_stream) + + +if PY2: + def _hash_py_argv(): + return zlib.crc32('\x00'.join(sys.argv[1:])) + + _initial_argv_hash = _hash_py_argv() + + def _get_windows_argv(): + argc = c_int(0) + argv_unicode = CommandLineToArgvW(GetCommandLineW(), byref(argc)) + argv = [argv_unicode[i] for i in range(0, argc.value)] + + if not hasattr(sys, 'frozen'): + argv = argv[1:] + while len(argv) > 0: + arg = argv[0] + if not arg.startswith('-') or arg == '-': + break + argv = argv[1:] + if arg.startswith(('-c', '-m')): + break + + return argv[1:] + + +_stream_factories = { + 0: _get_text_stdin, + 1: _get_text_stdout, + 2: _get_text_stderr, +} + + +def _get_windows_console_stream(f, encoding, errors): + if get_buffer is not None and \ + encoding in ('utf-16-le', None) \ + and errors in ('strict', None) and \ + hasattr(f, 'isatty') and f.isatty(): + func = _stream_factories.get(f.fileno()) + if func is not None: + if not PY2: + f = getattr(f, 'buffer', None) + if f is None: + return None + else: + # If we are on Python 2 we need to set the stream that we + # deal with to binary mode as otherwise the exercise if a + # bit moot. The same problems apply as for + # get_binary_stdin and friends from _compat. + msvcrt.setmode(f.fileno(), os.O_BINARY) + return func(f) diff --git a/libs/click/core.py b/libs/click/core.py new file mode 100644 index 00000000..7a1e3422 --- /dev/null +++ b/libs/click/core.py @@ -0,0 +1,1856 @@ +import errno +import inspect +import os +import sys +from contextlib import contextmanager +from itertools import repeat +from functools import update_wrapper + +from .types import convert_type, IntRange, BOOL +from .utils import PacifyFlushWrapper, make_str, make_default_short_help, \ + echo, get_os_args +from .exceptions import ClickException, UsageError, BadParameter, Abort, \ + MissingParameter, Exit +from .termui import prompt, confirm, style +from .formatting import HelpFormatter, join_options +from .parser import OptionParser, split_opt +from .globals import push_context, pop_context + +from ._compat import PY2, isidentifier, iteritems, string_types +from ._unicodefun import _check_for_unicode_literals, _verify_python3_env + + +_missing = object() + + +SUBCOMMAND_METAVAR = 'COMMAND [ARGS]...' +SUBCOMMANDS_METAVAR = 'COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...' + +DEPRECATED_HELP_NOTICE = ' (DEPRECATED)' +DEPRECATED_INVOKE_NOTICE = 'DeprecationWarning: ' + \ + 'The command %(name)s is deprecated.' + + +def _maybe_show_deprecated_notice(cmd): + if cmd.deprecated: + echo(style(DEPRECATED_INVOKE_NOTICE % {'name': cmd.name}, fg='red'), err=True) + + +def fast_exit(code): + """Exit without garbage collection, this speeds up exit by about 10ms for + things like bash completion. + """ + sys.stdout.flush() + sys.stderr.flush() + os._exit(code) + + +def _bashcomplete(cmd, prog_name, complete_var=None): + """Internal handler for the bash completion support.""" + if complete_var is None: + complete_var = '_%s_COMPLETE' % (prog_name.replace('-', '_')).upper() + complete_instr = os.environ.get(complete_var) + if not complete_instr: + return + + from ._bashcomplete import bashcomplete + if bashcomplete(cmd, prog_name, complete_var, complete_instr): + fast_exit(1) + + +def _check_multicommand(base_command, cmd_name, cmd, register=False): + if not base_command.chain or not isinstance(cmd, MultiCommand): + return + if register: + hint = 'It is not possible to add multi commands as children to ' \ + 'another multi command that is in chain mode' + else: + hint = 'Found a multi command as subcommand to a multi command ' \ + 'that is in chain mode. This is not supported' + raise RuntimeError('%s. Command "%s" is set to chain and "%s" was ' + 'added as subcommand but it in itself is a ' + 'multi command. ("%s" is a %s within a chained ' + '%s named "%s").' % ( + hint, base_command.name, cmd_name, + cmd_name, cmd.__class__.__name__, + base_command.__class__.__name__, + base_command.name)) + + +def batch(iterable, batch_size): + return list(zip(*repeat(iter(iterable), batch_size))) + + +def invoke_param_callback(callback, ctx, param, value): + code = getattr(callback, '__code__', None) + args = getattr(code, 'co_argcount', 3) + + if args < 3: + # This will become a warning in Click 3.0: + from warnings import warn + warn(Warning('Invoked legacy parameter callback "%s". The new ' + 'signature for such callbacks starting with ' + 'click 2.0 is (ctx, param, value).' + % callback), stacklevel=3) + return callback(ctx, value) + return callback(ctx, param, value) + + +@contextmanager +def augment_usage_errors(ctx, param=None): + """Context manager that attaches extra information to exceptions that + fly. + """ + try: + yield + except BadParameter as e: + if e.ctx is None: + e.ctx = ctx + if param is not None and e.param is None: + e.param = param + raise + except UsageError as e: + if e.ctx is None: + e.ctx = ctx + raise + + +def iter_params_for_processing(invocation_order, declaration_order): + """Given a sequence of parameters in the order as should be considered + for processing and an iterable of parameters that exist, this returns + a list in the correct order as they should be processed. + """ + def sort_key(item): + try: + idx = invocation_order.index(item) + except ValueError: + idx = float('inf') + return (not item.is_eager, idx) + + return sorted(declaration_order, key=sort_key) + + +class Context(object): + """The context is a special internal object that holds state relevant + for the script execution at every single level. It's normally invisible + to commands unless they opt-in to getting access to it. + + The context is useful as it can pass internal objects around and can + control special execution features such as reading data from + environment variables. + + A context can be used as context manager in which case it will call + :meth:`close` on teardown. + + .. versionadded:: 2.0 + Added the `resilient_parsing`, `help_option_names`, + `token_normalize_func` parameters. + + .. versionadded:: 3.0 + Added the `allow_extra_args` and `allow_interspersed_args` + parameters. + + .. versionadded:: 4.0 + Added the `color`, `ignore_unknown_options`, and + `max_content_width` parameters. + + :param command: the command class for this context. + :param parent: the parent context. + :param info_name: the info name for this invocation. Generally this + is the most descriptive name for the script or + command. For the toplevel script it is usually + the name of the script, for commands below it it's + the name of the script. + :param obj: an arbitrary object of user data. + :param auto_envvar_prefix: the prefix to use for automatic environment + variables. If this is `None` then reading + from environment variables is disabled. This + does not affect manually set environment + variables which are always read. + :param default_map: a dictionary (like object) with default values + for parameters. + :param terminal_width: the width of the terminal. The default is + inherit from parent context. If no context + defines the terminal width then auto + detection will be applied. + :param max_content_width: the maximum width for content rendered by + Click (this currently only affects help + pages). This defaults to 80 characters if + not overridden. In other words: even if the + terminal is larger than that, Click will not + format things wider than 80 characters by + default. In addition to that, formatters might + add some safety mapping on the right. + :param resilient_parsing: if this flag is enabled then Click will + parse without any interactivity or callback + invocation. Default values will also be + ignored. This is useful for implementing + things such as completion support. + :param allow_extra_args: if this is set to `True` then extra arguments + at the end will not raise an error and will be + kept on the context. The default is to inherit + from the command. + :param allow_interspersed_args: if this is set to `False` then options + and arguments cannot be mixed. The + default is to inherit from the command. + :param ignore_unknown_options: instructs click to ignore options it does + not know and keeps them for later + processing. + :param help_option_names: optionally a list of strings that define how + the default help parameter is named. The + default is ``['--help']``. + :param token_normalize_func: an optional function that is used to + normalize tokens (options, choices, + etc.). This for instance can be used to + implement case insensitive behavior. + :param color: controls if the terminal supports ANSI colors or not. The + default is autodetection. This is only needed if ANSI + codes are used in texts that Click prints which is by + default not the case. This for instance would affect + help output. + """ + + def __init__(self, command, parent=None, info_name=None, obj=None, + auto_envvar_prefix=None, default_map=None, + terminal_width=None, max_content_width=None, + resilient_parsing=False, allow_extra_args=None, + allow_interspersed_args=None, + ignore_unknown_options=None, help_option_names=None, + token_normalize_func=None, color=None): + #: the parent context or `None` if none exists. + self.parent = parent + #: the :class:`Command` for this context. + self.command = command + #: the descriptive information name + self.info_name = info_name + #: the parsed parameters except if the value is hidden in which + #: case it's not remembered. + self.params = {} + #: the leftover arguments. + self.args = [] + #: protected arguments. These are arguments that are prepended + #: to `args` when certain parsing scenarios are encountered but + #: must be never propagated to another arguments. This is used + #: to implement nested parsing. + self.protected_args = [] + if obj is None and parent is not None: + obj = parent.obj + #: the user object stored. + self.obj = obj + self._meta = getattr(parent, 'meta', {}) + + #: A dictionary (-like object) with defaults for parameters. + if default_map is None \ + and parent is not None \ + and parent.default_map is not None: + default_map = parent.default_map.get(info_name) + self.default_map = default_map + + #: This flag indicates if a subcommand is going to be executed. A + #: group callback can use this information to figure out if it's + #: being executed directly or because the execution flow passes + #: onwards to a subcommand. By default it's None, but it can be + #: the name of the subcommand to execute. + #: + #: If chaining is enabled this will be set to ``'*'`` in case + #: any commands are executed. It is however not possible to + #: figure out which ones. If you require this knowledge you + #: should use a :func:`resultcallback`. + self.invoked_subcommand = None + + if terminal_width is None and parent is not None: + terminal_width = parent.terminal_width + #: The width of the terminal (None is autodetection). + self.terminal_width = terminal_width + + if max_content_width is None and parent is not None: + max_content_width = parent.max_content_width + #: The maximum width of formatted content (None implies a sensible + #: default which is 80 for most things). + self.max_content_width = max_content_width + + if allow_extra_args is None: + allow_extra_args = command.allow_extra_args + #: Indicates if the context allows extra args or if it should + #: fail on parsing. + #: + #: .. versionadded:: 3.0 + self.allow_extra_args = allow_extra_args + + if allow_interspersed_args is None: + allow_interspersed_args = command.allow_interspersed_args + #: Indicates if the context allows mixing of arguments and + #: options or not. + #: + #: .. versionadded:: 3.0 + self.allow_interspersed_args = allow_interspersed_args + + if ignore_unknown_options is None: + ignore_unknown_options = command.ignore_unknown_options + #: Instructs click to ignore options that a command does not + #: understand and will store it on the context for later + #: processing. This is primarily useful for situations where you + #: want to call into external programs. Generally this pattern is + #: strongly discouraged because it's not possibly to losslessly + #: forward all arguments. + #: + #: .. versionadded:: 4.0 + self.ignore_unknown_options = ignore_unknown_options + + if help_option_names is None: + if parent is not None: + help_option_names = parent.help_option_names + else: + help_option_names = ['--help'] + + #: The names for the help options. + self.help_option_names = help_option_names + + if token_normalize_func is None and parent is not None: + token_normalize_func = parent.token_normalize_func + + #: An optional normalization function for tokens. This is + #: options, choices, commands etc. + self.token_normalize_func = token_normalize_func + + #: Indicates if resilient parsing is enabled. In that case Click + #: will do its best to not cause any failures and default values + #: will be ignored. Useful for completion. + self.resilient_parsing = resilient_parsing + + # If there is no envvar prefix yet, but the parent has one and + # the command on this level has a name, we can expand the envvar + # prefix automatically. + if auto_envvar_prefix is None: + if parent is not None \ + and parent.auto_envvar_prefix is not None and \ + self.info_name is not None: + auto_envvar_prefix = '%s_%s' % (parent.auto_envvar_prefix, + self.info_name.upper()) + else: + auto_envvar_prefix = auto_envvar_prefix.upper() + self.auto_envvar_prefix = auto_envvar_prefix + + if color is None and parent is not None: + color = parent.color + + #: Controls if styling output is wanted or not. + self.color = color + + self._close_callbacks = [] + self._depth = 0 + + def __enter__(self): + self._depth += 1 + push_context(self) + return self + + def __exit__(self, exc_type, exc_value, tb): + self._depth -= 1 + if self._depth == 0: + self.close() + pop_context() + + @contextmanager + def scope(self, cleanup=True): + """This helper method can be used with the context object to promote + it to the current thread local (see :func:`get_current_context`). + The default behavior of this is to invoke the cleanup functions which + can be disabled by setting `cleanup` to `False`. The cleanup + functions are typically used for things such as closing file handles. + + If the cleanup is intended the context object can also be directly + used as a context manager. + + Example usage:: + + with ctx.scope(): + assert get_current_context() is ctx + + This is equivalent:: + + with ctx: + assert get_current_context() is ctx + + .. versionadded:: 5.0 + + :param cleanup: controls if the cleanup functions should be run or + not. The default is to run these functions. In + some situations the context only wants to be + temporarily pushed in which case this can be disabled. + Nested pushes automatically defer the cleanup. + """ + if not cleanup: + self._depth += 1 + try: + with self as rv: + yield rv + finally: + if not cleanup: + self._depth -= 1 + + @property + def meta(self): + """This is a dictionary which is shared with all the contexts + that are nested. It exists so that click utilities can store some + state here if they need to. It is however the responsibility of + that code to manage this dictionary well. + + The keys are supposed to be unique dotted strings. For instance + module paths are a good choice for it. What is stored in there is + irrelevant for the operation of click. However what is important is + that code that places data here adheres to the general semantics of + the system. + + Example usage:: + + LANG_KEY = __name__ + '.lang' + + def set_language(value): + ctx = get_current_context() + ctx.meta[LANG_KEY] = value + + def get_language(): + return get_current_context().meta.get(LANG_KEY, 'en_US') + + .. versionadded:: 5.0 + """ + return self._meta + + def make_formatter(self): + """Creates the formatter for the help and usage output.""" + return HelpFormatter(width=self.terminal_width, + max_width=self.max_content_width) + + def call_on_close(self, f): + """This decorator remembers a function as callback that should be + executed when the context tears down. This is most useful to bind + resource handling to the script execution. For instance, file objects + opened by the :class:`File` type will register their close callbacks + here. + + :param f: the function to execute on teardown. + """ + self._close_callbacks.append(f) + return f + + def close(self): + """Invokes all close callbacks.""" + for cb in self._close_callbacks: + cb() + self._close_callbacks = [] + + @property + def command_path(self): + """The computed command path. This is used for the ``usage`` + information on the help page. It's automatically created by + combining the info names of the chain of contexts to the root. + """ + rv = '' + if self.info_name is not None: + rv = self.info_name + if self.parent is not None: + rv = self.parent.command_path + ' ' + rv + return rv.lstrip() + + def find_root(self): + """Finds the outermost context.""" + node = self + while node.parent is not None: + node = node.parent + return node + + def find_object(self, object_type): + """Finds the closest object of a given type.""" + node = self + while node is not None: + if isinstance(node.obj, object_type): + return node.obj + node = node.parent + + def ensure_object(self, object_type): + """Like :meth:`find_object` but sets the innermost object to a + new instance of `object_type` if it does not exist. + """ + rv = self.find_object(object_type) + if rv is None: + self.obj = rv = object_type() + return rv + + def lookup_default(self, name): + """Looks up the default for a parameter name. This by default + looks into the :attr:`default_map` if available. + """ + if self.default_map is not None: + rv = self.default_map.get(name) + if callable(rv): + rv = rv() + return rv + + def fail(self, message): + """Aborts the execution of the program with a specific error + message. + + :param message: the error message to fail with. + """ + raise UsageError(message, self) + + def abort(self): + """Aborts the script.""" + raise Abort() + + def exit(self, code=0): + """Exits the application with a given exit code.""" + raise Exit(code) + + def get_usage(self): + """Helper method to get formatted usage string for the current + context and command. + """ + return self.command.get_usage(self) + + def get_help(self): + """Helper method to get formatted help page for the current + context and command. + """ + return self.command.get_help(self) + + def invoke(*args, **kwargs): + """Invokes a command callback in exactly the way it expects. There + are two ways to invoke this method: + + 1. the first argument can be a callback and all other arguments and + keyword arguments are forwarded directly to the function. + 2. the first argument is a click command object. In that case all + arguments are forwarded as well but proper click parameters + (options and click arguments) must be keyword arguments and Click + will fill in defaults. + + Note that before Click 3.2 keyword arguments were not properly filled + in against the intention of this code and no context was created. For + more information about this change and why it was done in a bugfix + release see :ref:`upgrade-to-3.2`. + """ + self, callback = args[:2] + ctx = self + + # It's also possible to invoke another command which might or + # might not have a callback. In that case we also fill + # in defaults and make a new context for this command. + if isinstance(callback, Command): + other_cmd = callback + callback = other_cmd.callback + ctx = Context(other_cmd, info_name=other_cmd.name, parent=self) + if callback is None: + raise TypeError('The given command does not have a ' + 'callback that can be invoked.') + + for param in other_cmd.params: + if param.name not in kwargs and param.expose_value: + kwargs[param.name] = param.get_default(ctx) + + args = args[2:] + with augment_usage_errors(self): + with ctx: + return callback(*args, **kwargs) + + def forward(*args, **kwargs): + """Similar to :meth:`invoke` but fills in default keyword + arguments from the current context if the other command expects + it. This cannot invoke callbacks directly, only other commands. + """ + self, cmd = args[:2] + + # It's also possible to invoke another command which might or + # might not have a callback. + if not isinstance(cmd, Command): + raise TypeError('Callback is not a command.') + + for param in self.params: + if param not in kwargs: + kwargs[param] = self.params[param] + + return self.invoke(cmd, **kwargs) + + +class BaseCommand(object): + """The base command implements the minimal API contract of commands. + Most code will never use this as it does not implement a lot of useful + functionality but it can act as the direct subclass of alternative + parsing methods that do not depend on the Click parser. + + For instance, this can be used to bridge Click and other systems like + argparse or docopt. + + Because base commands do not implement a lot of the API that other + parts of Click take for granted, they are not supported for all + operations. For instance, they cannot be used with the decorators + usually and they have no built-in callback system. + + .. versionchanged:: 2.0 + Added the `context_settings` parameter. + + :param name: the name of the command to use unless a group overrides it. + :param context_settings: an optional dictionary with defaults that are + passed to the context object. + """ + #: the default for the :attr:`Context.allow_extra_args` flag. + allow_extra_args = False + #: the default for the :attr:`Context.allow_interspersed_args` flag. + allow_interspersed_args = True + #: the default for the :attr:`Context.ignore_unknown_options` flag. + ignore_unknown_options = False + + def __init__(self, name, context_settings=None): + #: the name the command thinks it has. Upon registering a command + #: on a :class:`Group` the group will default the command name + #: with this information. You should instead use the + #: :class:`Context`\'s :attr:`~Context.info_name` attribute. + self.name = name + if context_settings is None: + context_settings = {} + #: an optional dictionary with defaults passed to the context. + self.context_settings = context_settings + + def get_usage(self, ctx): + raise NotImplementedError('Base commands cannot get usage') + + def get_help(self, ctx): + raise NotImplementedError('Base commands cannot get help') + + def make_context(self, info_name, args, parent=None, **extra): + """This function when given an info name and arguments will kick + off the parsing and create a new :class:`Context`. It does not + invoke the actual command callback though. + + :param info_name: the info name for this invokation. Generally this + is the most descriptive name for the script or + command. For the toplevel script it's usually + the name of the script, for commands below it it's + the name of the script. + :param args: the arguments to parse as list of strings. + :param parent: the parent context if available. + :param extra: extra keyword arguments forwarded to the context + constructor. + """ + for key, value in iteritems(self.context_settings): + if key not in extra: + extra[key] = value + ctx = Context(self, info_name=info_name, parent=parent, **extra) + with ctx.scope(cleanup=False): + self.parse_args(ctx, args) + return ctx + + def parse_args(self, ctx, args): + """Given a context and a list of arguments this creates the parser + and parses the arguments, then modifies the context as necessary. + This is automatically invoked by :meth:`make_context`. + """ + raise NotImplementedError('Base commands do not know how to parse ' + 'arguments.') + + def invoke(self, ctx): + """Given a context, this invokes the command. The default + implementation is raising a not implemented error. + """ + raise NotImplementedError('Base commands are not invokable by default') + + def main(self, args=None, prog_name=None, complete_var=None, + standalone_mode=True, **extra): + """This is the way to invoke a script with all the bells and + whistles as a command line application. This will always terminate + the application after a call. If this is not wanted, ``SystemExit`` + needs to be caught. + + This method is also available by directly calling the instance of + a :class:`Command`. + + .. versionadded:: 3.0 + Added the `standalone_mode` flag to control the standalone mode. + + :param args: the arguments that should be used for parsing. If not + provided, ``sys.argv[1:]`` is used. + :param prog_name: the program name that should be used. By default + the program name is constructed by taking the file + name from ``sys.argv[0]``. + :param complete_var: the environment variable that controls the + bash completion support. The default is + ``"__COMPLETE"`` with prog_name in + uppercase. + :param standalone_mode: the default behavior is to invoke the script + in standalone mode. Click will then + handle exceptions and convert them into + error messages and the function will never + return but shut down the interpreter. If + this is set to `False` they will be + propagated to the caller and the return + value of this function is the return value + of :meth:`invoke`. + :param extra: extra keyword arguments are forwarded to the context + constructor. See :class:`Context` for more information. + """ + # If we are in Python 3, we will verify that the environment is + # sane at this point or reject further execution to avoid a + # broken script. + if not PY2: + _verify_python3_env() + else: + _check_for_unicode_literals() + + if args is None: + args = get_os_args() + else: + args = list(args) + + if prog_name is None: + prog_name = make_str(os.path.basename( + sys.argv and sys.argv[0] or __file__)) + + # Hook for the Bash completion. This only activates if the Bash + # completion is actually enabled, otherwise this is quite a fast + # noop. + _bashcomplete(self, prog_name, complete_var) + + try: + try: + with self.make_context(prog_name, args, **extra) as ctx: + rv = self.invoke(ctx) + if not standalone_mode: + return rv + # it's not safe to `ctx.exit(rv)` here! + # note that `rv` may actually contain data like "1" which + # has obvious effects + # more subtle case: `rv=[None, None]` can come out of + # chained commands which all returned `None` -- so it's not + # even always obvious that `rv` indicates success/failure + # by its truthiness/falsiness + ctx.exit() + except (EOFError, KeyboardInterrupt): + echo(file=sys.stderr) + raise Abort() + except ClickException as e: + if not standalone_mode: + raise + e.show() + sys.exit(e.exit_code) + except IOError as e: + if e.errno == errno.EPIPE: + sys.stdout = PacifyFlushWrapper(sys.stdout) + sys.stderr = PacifyFlushWrapper(sys.stderr) + sys.exit(1) + else: + raise + except Exit as e: + if standalone_mode: + sys.exit(e.exit_code) + else: + # in non-standalone mode, return the exit code + # note that this is only reached if `self.invoke` above raises + # an Exit explicitly -- thus bypassing the check there which + # would return its result + # the results of non-standalone execution may therefore be + # somewhat ambiguous: if there are codepaths which lead to + # `ctx.exit(1)` and to `return 1`, the caller won't be able to + # tell the difference between the two + return e.exit_code + except Abort: + if not standalone_mode: + raise + echo('Aborted!', file=sys.stderr) + sys.exit(1) + + def __call__(self, *args, **kwargs): + """Alias for :meth:`main`.""" + return self.main(*args, **kwargs) + + +class Command(BaseCommand): + """Commands are the basic building block of command line interfaces in + Click. A basic command handles command line parsing and might dispatch + more parsing to commands nested below it. + + .. versionchanged:: 2.0 + Added the `context_settings` parameter. + + :param name: the name of the command to use unless a group overrides it. + :param context_settings: an optional dictionary with defaults that are + passed to the context object. + :param callback: the callback to invoke. This is optional. + :param params: the parameters to register with this command. This can + be either :class:`Option` or :class:`Argument` objects. + :param help: the help string to use for this command. + :param epilog: like the help string but it's printed at the end of the + help page after everything else. + :param short_help: the short help to use for this command. This is + shown on the command listing of the parent command. + :param add_help_option: by default each command registers a ``--help`` + option. This can be disabled by this parameter. + :param hidden: hide this command from help outputs. + + :param deprecated: issues a message indicating that + the command is deprecated. + """ + + def __init__(self, name, context_settings=None, callback=None, + params=None, help=None, epilog=None, short_help=None, + options_metavar='[OPTIONS]', add_help_option=True, + hidden=False, deprecated=False): + BaseCommand.__init__(self, name, context_settings) + #: the callback to execute when the command fires. This might be + #: `None` in which case nothing happens. + self.callback = callback + #: the list of parameters for this command in the order they + #: should show up in the help page and execute. Eager parameters + #: will automatically be handled before non eager ones. + self.params = params or [] + # if a form feed (page break) is found in the help text, truncate help + # text to the content preceding the first form feed + if help and '\f' in help: + help = help.split('\f', 1)[0] + self.help = help + self.epilog = epilog + self.options_metavar = options_metavar + self.short_help = short_help + self.add_help_option = add_help_option + self.hidden = hidden + self.deprecated = deprecated + + def get_usage(self, ctx): + formatter = ctx.make_formatter() + self.format_usage(ctx, formatter) + return formatter.getvalue().rstrip('\n') + + def get_params(self, ctx): + rv = self.params + help_option = self.get_help_option(ctx) + if help_option is not None: + rv = rv + [help_option] + return rv + + def format_usage(self, ctx, formatter): + """Writes the usage line into the formatter.""" + pieces = self.collect_usage_pieces(ctx) + formatter.write_usage(ctx.command_path, ' '.join(pieces)) + + def collect_usage_pieces(self, ctx): + """Returns all the pieces that go into the usage line and returns + it as a list of strings. + """ + rv = [self.options_metavar] + for param in self.get_params(ctx): + rv.extend(param.get_usage_pieces(ctx)) + return rv + + def get_help_option_names(self, ctx): + """Returns the names for the help option.""" + all_names = set(ctx.help_option_names) + for param in self.params: + all_names.difference_update(param.opts) + all_names.difference_update(param.secondary_opts) + return all_names + + def get_help_option(self, ctx): + """Returns the help option object.""" + help_options = self.get_help_option_names(ctx) + if not help_options or not self.add_help_option: + return + + def show_help(ctx, param, value): + if value and not ctx.resilient_parsing: + echo(ctx.get_help(), color=ctx.color) + ctx.exit() + return Option(help_options, is_flag=True, + is_eager=True, expose_value=False, + callback=show_help, + help='Show this message and exit.') + + def make_parser(self, ctx): + """Creates the underlying option parser for this command.""" + parser = OptionParser(ctx) + for param in self.get_params(ctx): + param.add_to_parser(parser, ctx) + return parser + + def get_help(self, ctx): + """Formats the help into a string and returns it. This creates a + formatter and will call into the following formatting methods: + """ + formatter = ctx.make_formatter() + self.format_help(ctx, formatter) + return formatter.getvalue().rstrip('\n') + + def get_short_help_str(self, limit=45): + """Gets short help for the command or makes it by shortening the long help string.""" + return self.short_help or self.help and make_default_short_help(self.help, limit) or '' + + def format_help(self, ctx, formatter): + """Writes the help into the formatter if it exists. + + This calls into the following methods: + + - :meth:`format_usage` + - :meth:`format_help_text` + - :meth:`format_options` + - :meth:`format_epilog` + """ + self.format_usage(ctx, formatter) + self.format_help_text(ctx, formatter) + self.format_options(ctx, formatter) + self.format_epilog(ctx, formatter) + + def format_help_text(self, ctx, formatter): + """Writes the help text to the formatter if it exists.""" + if self.help: + formatter.write_paragraph() + with formatter.indentation(): + help_text = self.help + if self.deprecated: + help_text += DEPRECATED_HELP_NOTICE + formatter.write_text(help_text) + elif self.deprecated: + formatter.write_paragraph() + with formatter.indentation(): + formatter.write_text(DEPRECATED_HELP_NOTICE) + + def format_options(self, ctx, formatter): + """Writes all the options into the formatter if they exist.""" + opts = [] + for param in self.get_params(ctx): + rv = param.get_help_record(ctx) + if rv is not None: + opts.append(rv) + + if opts: + with formatter.section('Options'): + formatter.write_dl(opts) + + def format_epilog(self, ctx, formatter): + """Writes the epilog into the formatter if it exists.""" + if self.epilog: + formatter.write_paragraph() + with formatter.indentation(): + formatter.write_text(self.epilog) + + def parse_args(self, ctx, args): + parser = self.make_parser(ctx) + opts, args, param_order = parser.parse_args(args=args) + + for param in iter_params_for_processing( + param_order, self.get_params(ctx)): + value, args = param.handle_parse_result(ctx, opts, args) + + if args and not ctx.allow_extra_args and not ctx.resilient_parsing: + ctx.fail('Got unexpected extra argument%s (%s)' + % (len(args) != 1 and 's' or '', + ' '.join(map(make_str, args)))) + + ctx.args = args + return args + + def invoke(self, ctx): + """Given a context, this invokes the attached callback (if it exists) + in the right way. + """ + _maybe_show_deprecated_notice(self) + if self.callback is not None: + return ctx.invoke(self.callback, **ctx.params) + + +class MultiCommand(Command): + """A multi command is the basic implementation of a command that + dispatches to subcommands. The most common version is the + :class:`Group`. + + :param invoke_without_command: this controls how the multi command itself + is invoked. By default it's only invoked + if a subcommand is provided. + :param no_args_is_help: this controls what happens if no arguments are + provided. This option is enabled by default if + `invoke_without_command` is disabled or disabled + if it's enabled. If enabled this will add + ``--help`` as argument if no arguments are + passed. + :param subcommand_metavar: the string that is used in the documentation + to indicate the subcommand place. + :param chain: if this is set to `True` chaining of multiple subcommands + is enabled. This restricts the form of commands in that + they cannot have optional arguments but it allows + multiple commands to be chained together. + :param result_callback: the result callback to attach to this multi + command. + """ + allow_extra_args = True + allow_interspersed_args = False + + def __init__(self, name=None, invoke_without_command=False, + no_args_is_help=None, subcommand_metavar=None, + chain=False, result_callback=None, **attrs): + Command.__init__(self, name, **attrs) + if no_args_is_help is None: + no_args_is_help = not invoke_without_command + self.no_args_is_help = no_args_is_help + self.invoke_without_command = invoke_without_command + if subcommand_metavar is None: + if chain: + subcommand_metavar = SUBCOMMANDS_METAVAR + else: + subcommand_metavar = SUBCOMMAND_METAVAR + self.subcommand_metavar = subcommand_metavar + self.chain = chain + #: The result callback that is stored. This can be set or + #: overridden with the :func:`resultcallback` decorator. + self.result_callback = result_callback + + if self.chain: + for param in self.params: + if isinstance(param, Argument) and not param.required: + raise RuntimeError('Multi commands in chain mode cannot ' + 'have optional arguments.') + + def collect_usage_pieces(self, ctx): + rv = Command.collect_usage_pieces(self, ctx) + rv.append(self.subcommand_metavar) + return rv + + def format_options(self, ctx, formatter): + Command.format_options(self, ctx, formatter) + self.format_commands(ctx, formatter) + + def resultcallback(self, replace=False): + """Adds a result callback to the chain command. By default if a + result callback is already registered this will chain them but + this can be disabled with the `replace` parameter. The result + callback is invoked with the return value of the subcommand + (or the list of return values from all subcommands if chaining + is enabled) as well as the parameters as they would be passed + to the main callback. + + Example:: + + @click.group() + @click.option('-i', '--input', default=23) + def cli(input): + return 42 + + @cli.resultcallback() + def process_result(result, input): + return result + input + + .. versionadded:: 3.0 + + :param replace: if set to `True` an already existing result + callback will be removed. + """ + def decorator(f): + old_callback = self.result_callback + if old_callback is None or replace: + self.result_callback = f + return f + def function(__value, *args, **kwargs): + return f(old_callback(__value, *args, **kwargs), + *args, **kwargs) + self.result_callback = rv = update_wrapper(function, f) + return rv + return decorator + + def format_commands(self, ctx, formatter): + """Extra format methods for multi methods that adds all the commands + after the options. + """ + commands = [] + for subcommand in self.list_commands(ctx): + cmd = self.get_command(ctx, subcommand) + # What is this, the tool lied about a command. Ignore it + if cmd is None: + continue + if cmd.hidden: + continue + + commands.append((subcommand, cmd)) + + # allow for 3 times the default spacing + if len(commands): + limit = formatter.width - 6 - max(len(cmd[0]) for cmd in commands) + + rows = [] + for subcommand, cmd in commands: + help = cmd.get_short_help_str(limit) + rows.append((subcommand, help)) + + if rows: + with formatter.section('Commands'): + formatter.write_dl(rows) + + def parse_args(self, ctx, args): + if not args and self.no_args_is_help and not ctx.resilient_parsing: + echo(ctx.get_help(), color=ctx.color) + ctx.exit() + + rest = Command.parse_args(self, ctx, args) + if self.chain: + ctx.protected_args = rest + ctx.args = [] + elif rest: + ctx.protected_args, ctx.args = rest[:1], rest[1:] + + return ctx.args + + def invoke(self, ctx): + def _process_result(value): + if self.result_callback is not None: + value = ctx.invoke(self.result_callback, value, + **ctx.params) + return value + + if not ctx.protected_args: + # If we are invoked without command the chain flag controls + # how this happens. If we are not in chain mode, the return + # value here is the return value of the command. + # If however we are in chain mode, the return value is the + # return value of the result processor invoked with an empty + # list (which means that no subcommand actually was executed). + if self.invoke_without_command: + if not self.chain: + return Command.invoke(self, ctx) + with ctx: + Command.invoke(self, ctx) + return _process_result([]) + ctx.fail('Missing command.') + + # Fetch args back out + args = ctx.protected_args + ctx.args + ctx.args = [] + ctx.protected_args = [] + + # If we're not in chain mode, we only allow the invocation of a + # single command but we also inform the current context about the + # name of the command to invoke. + if not self.chain: + # Make sure the context is entered so we do not clean up + # resources until the result processor has worked. + with ctx: + cmd_name, cmd, args = self.resolve_command(ctx, args) + ctx.invoked_subcommand = cmd_name + Command.invoke(self, ctx) + sub_ctx = cmd.make_context(cmd_name, args, parent=ctx) + with sub_ctx: + return _process_result(sub_ctx.command.invoke(sub_ctx)) + + # In chain mode we create the contexts step by step, but after the + # base command has been invoked. Because at that point we do not + # know the subcommands yet, the invoked subcommand attribute is + # set to ``*`` to inform the command that subcommands are executed + # but nothing else. + with ctx: + ctx.invoked_subcommand = args and '*' or None + Command.invoke(self, ctx) + + # Otherwise we make every single context and invoke them in a + # chain. In that case the return value to the result processor + # is the list of all invoked subcommand's results. + contexts = [] + while args: + cmd_name, cmd, args = self.resolve_command(ctx, args) + sub_ctx = cmd.make_context(cmd_name, args, parent=ctx, + allow_extra_args=True, + allow_interspersed_args=False) + contexts.append(sub_ctx) + args, sub_ctx.args = sub_ctx.args, [] + + rv = [] + for sub_ctx in contexts: + with sub_ctx: + rv.append(sub_ctx.command.invoke(sub_ctx)) + return _process_result(rv) + + def resolve_command(self, ctx, args): + cmd_name = make_str(args[0]) + original_cmd_name = cmd_name + + # Get the command + cmd = self.get_command(ctx, cmd_name) + + # If we can't find the command but there is a normalization + # function available, we try with that one. + if cmd is None and ctx.token_normalize_func is not None: + cmd_name = ctx.token_normalize_func(cmd_name) + cmd = self.get_command(ctx, cmd_name) + + # If we don't find the command we want to show an error message + # to the user that it was not provided. However, there is + # something else we should do: if the first argument looks like + # an option we want to kick off parsing again for arguments to + # resolve things like --help which now should go to the main + # place. + if cmd is None and not ctx.resilient_parsing: + if split_opt(cmd_name)[0]: + self.parse_args(ctx, ctx.args) + ctx.fail('No such command "%s".' % original_cmd_name) + + return cmd_name, cmd, args[1:] + + def get_command(self, ctx, cmd_name): + """Given a context and a command name, this returns a + :class:`Command` object if it exists or returns `None`. + """ + raise NotImplementedError() + + def list_commands(self, ctx): + """Returns a list of subcommand names in the order they should + appear. + """ + return [] + + +class Group(MultiCommand): + """A group allows a command to have subcommands attached. This is the + most common way to implement nesting in Click. + + :param commands: a dictionary of commands. + """ + + def __init__(self, name=None, commands=None, **attrs): + MultiCommand.__init__(self, name, **attrs) + #: the registered subcommands by their exported names. + self.commands = commands or {} + + def add_command(self, cmd, name=None): + """Registers another :class:`Command` with this group. If the name + is not provided, the name of the command is used. + """ + name = name or cmd.name + if name is None: + raise TypeError('Command has no name.') + _check_multicommand(self, name, cmd, register=True) + self.commands[name] = cmd + + def command(self, *args, **kwargs): + """A shortcut decorator for declaring and attaching a command to + the group. This takes the same arguments as :func:`command` but + immediately registers the created command with this instance by + calling into :meth:`add_command`. + """ + def decorator(f): + cmd = command(*args, **kwargs)(f) + self.add_command(cmd) + return cmd + return decorator + + def group(self, *args, **kwargs): + """A shortcut decorator for declaring and attaching a group to + the group. This takes the same arguments as :func:`group` but + immediately registers the created command with this instance by + calling into :meth:`add_command`. + """ + def decorator(f): + cmd = group(*args, **kwargs)(f) + self.add_command(cmd) + return cmd + return decorator + + def get_command(self, ctx, cmd_name): + return self.commands.get(cmd_name) + + def list_commands(self, ctx): + return sorted(self.commands) + + +class CommandCollection(MultiCommand): + """A command collection is a multi command that merges multiple multi + commands together into one. This is a straightforward implementation + that accepts a list of different multi commands as sources and + provides all the commands for each of them. + """ + + def __init__(self, name=None, sources=None, **attrs): + MultiCommand.__init__(self, name, **attrs) + #: The list of registered multi commands. + self.sources = sources or [] + + def add_source(self, multi_cmd): + """Adds a new multi command to the chain dispatcher.""" + self.sources.append(multi_cmd) + + def get_command(self, ctx, cmd_name): + for source in self.sources: + rv = source.get_command(ctx, cmd_name) + if rv is not None: + if self.chain: + _check_multicommand(self, cmd_name, rv) + return rv + + def list_commands(self, ctx): + rv = set() + for source in self.sources: + rv.update(source.list_commands(ctx)) + return sorted(rv) + + +class Parameter(object): + r"""A parameter to a command comes in two versions: they are either + :class:`Option`\s or :class:`Argument`\s. Other subclasses are currently + not supported by design as some of the internals for parsing are + intentionally not finalized. + + Some settings are supported by both options and arguments. + + .. versionchanged:: 2.0 + Changed signature for parameter callback to also be passed the + parameter. In Click 2.0, the old callback format will still work, + but it will raise a warning to give you change to migrate the + code easier. + + :param param_decls: the parameter declarations for this option or + argument. This is a list of flags or argument + names. + :param type: the type that should be used. Either a :class:`ParamType` + or a Python type. The later is converted into the former + automatically if supported. + :param required: controls if this is optional or not. + :param default: the default value if omitted. This can also be a callable, + in which case it's invoked when the default is needed + without any arguments. + :param callback: a callback that should be executed after the parameter + was matched. This is called as ``fn(ctx, param, + value)`` and needs to return the value. Before Click + 2.0, the signature was ``(ctx, value)``. + :param nargs: the number of arguments to match. If not ``1`` the return + value is a tuple instead of single value. The default for + nargs is ``1`` (except if the type is a tuple, then it's + the arity of the tuple). + :param metavar: how the value is represented in the help page. + :param expose_value: if this is `True` then the value is passed onwards + to the command callback and stored on the context, + otherwise it's skipped. + :param is_eager: eager values are processed before non eager ones. This + should not be set for arguments or it will inverse the + order of processing. + :param envvar: a string or list of strings that are environment variables + that should be checked. + """ + param_type_name = 'parameter' + + def __init__(self, param_decls=None, type=None, required=False, + default=None, callback=None, nargs=None, metavar=None, + expose_value=True, is_eager=False, envvar=None, + autocompletion=None): + self.name, self.opts, self.secondary_opts = \ + self._parse_decls(param_decls or (), expose_value) + + self.type = convert_type(type, default) + + # Default nargs to what the type tells us if we have that + # information available. + if nargs is None: + if self.type.is_composite: + nargs = self.type.arity + else: + nargs = 1 + + self.required = required + self.callback = callback + self.nargs = nargs + self.multiple = False + self.expose_value = expose_value + self.default = default + self.is_eager = is_eager + self.metavar = metavar + self.envvar = envvar + self.autocompletion = autocompletion + + @property + def human_readable_name(self): + """Returns the human readable name of this parameter. This is the + same as the name for options, but the metavar for arguments. + """ + return self.name + + def make_metavar(self): + if self.metavar is not None: + return self.metavar + metavar = self.type.get_metavar(self) + if metavar is None: + metavar = self.type.name.upper() + if self.nargs != 1: + metavar += '...' + return metavar + + def get_default(self, ctx): + """Given a context variable this calculates the default value.""" + # Otherwise go with the regular default. + if callable(self.default): + rv = self.default() + else: + rv = self.default + return self.type_cast_value(ctx, rv) + + def add_to_parser(self, parser, ctx): + pass + + def consume_value(self, ctx, opts): + value = opts.get(self.name) + if value is None: + value = self.value_from_envvar(ctx) + if value is None: + value = ctx.lookup_default(self.name) + return value + + def type_cast_value(self, ctx, value): + """Given a value this runs it properly through the type system. + This automatically handles things like `nargs` and `multiple` as + well as composite types. + """ + if self.type.is_composite: + if self.nargs <= 1: + raise TypeError('Attempted to invoke composite type ' + 'but nargs has been set to %s. This is ' + 'not supported; nargs needs to be set to ' + 'a fixed value > 1.' % self.nargs) + if self.multiple: + return tuple(self.type(x or (), self, ctx) for x in value or ()) + return self.type(value or (), self, ctx) + + def _convert(value, level): + if level == 0: + return self.type(value, self, ctx) + return tuple(_convert(x, level - 1) for x in value or ()) + return _convert(value, (self.nargs != 1) + bool(self.multiple)) + + def process_value(self, ctx, value): + """Given a value and context this runs the logic to convert the + value as necessary. + """ + # If the value we were given is None we do nothing. This way + # code that calls this can easily figure out if something was + # not provided. Otherwise it would be converted into an empty + # tuple for multiple invocations which is inconvenient. + if value is not None: + return self.type_cast_value(ctx, value) + + def value_is_missing(self, value): + if value is None: + return True + if (self.nargs != 1 or self.multiple) and value == (): + return True + return False + + def full_process_value(self, ctx, value): + value = self.process_value(ctx, value) + + if value is None and not ctx.resilient_parsing: + value = self.get_default(ctx) + + if self.required and self.value_is_missing(value): + raise MissingParameter(ctx=ctx, param=self) + + return value + + def resolve_envvar_value(self, ctx): + if self.envvar is None: + return + if isinstance(self.envvar, (tuple, list)): + for envvar in self.envvar: + rv = os.environ.get(envvar) + if rv is not None: + return rv + else: + return os.environ.get(self.envvar) + + def value_from_envvar(self, ctx): + rv = self.resolve_envvar_value(ctx) + if rv is not None and self.nargs != 1: + rv = self.type.split_envvar_value(rv) + return rv + + def handle_parse_result(self, ctx, opts, args): + with augment_usage_errors(ctx, param=self): + value = self.consume_value(ctx, opts) + try: + value = self.full_process_value(ctx, value) + except Exception: + if not ctx.resilient_parsing: + raise + value = None + if self.callback is not None: + try: + value = invoke_param_callback( + self.callback, ctx, self, value) + except Exception: + if not ctx.resilient_parsing: + raise + + if self.expose_value: + ctx.params[self.name] = value + return value, args + + def get_help_record(self, ctx): + pass + + def get_usage_pieces(self, ctx): + return [] + + def get_error_hint(self, ctx): + """Get a stringified version of the param for use in error messages to + indicate which param caused the error. + """ + hint_list = self.opts or [self.human_readable_name] + return ' / '.join('"%s"' % x for x in hint_list) + + +class Option(Parameter): + """Options are usually optional values on the command line and + have some extra features that arguments don't have. + + All other parameters are passed onwards to the parameter constructor. + + :param show_default: controls if the default value should be shown on the + help page. Normally, defaults are not shown. If this + value is a string, it shows the string instead of the + value. This is particularly useful for dynamic options. + :param show_envvar: controls if an environment variable should be shown on + the help page. Normally, environment variables + are not shown. + :param prompt: if set to `True` or a non empty string then the user will be + prompted for input. If set to `True` the prompt will be the + option name capitalized. + :param confirmation_prompt: if set then the value will need to be confirmed + if it was prompted for. + :param hide_input: if this is `True` then the input on the prompt will be + hidden from the user. This is useful for password + input. + :param is_flag: forces this option to act as a flag. The default is + auto detection. + :param flag_value: which value should be used for this flag if it's + enabled. This is set to a boolean automatically if + the option string contains a slash to mark two options. + :param multiple: if this is set to `True` then the argument is accepted + multiple times and recorded. This is similar to ``nargs`` + in how it works but supports arbitrary number of + arguments. + :param count: this flag makes an option increment an integer. + :param allow_from_autoenv: if this is enabled then the value of this + parameter will be pulled from an environment + variable in case a prefix is defined on the + context. + :param help: the help string. + :param hidden: hide this option from help outputs. + """ + param_type_name = 'option' + + def __init__(self, param_decls=None, show_default=False, + prompt=False, confirmation_prompt=False, + hide_input=False, is_flag=None, flag_value=None, + multiple=False, count=False, allow_from_autoenv=True, + type=None, help=None, hidden=False, show_choices=True, + show_envvar=False, **attrs): + default_is_missing = attrs.get('default', _missing) is _missing + Parameter.__init__(self, param_decls, type=type, **attrs) + + if prompt is True: + prompt_text = self.name.replace('_', ' ').capitalize() + elif prompt is False: + prompt_text = None + else: + prompt_text = prompt + self.prompt = prompt_text + self.confirmation_prompt = confirmation_prompt + self.hide_input = hide_input + self.hidden = hidden + + # Flags + if is_flag is None: + if flag_value is not None: + is_flag = True + else: + is_flag = bool(self.secondary_opts) + if is_flag and default_is_missing: + self.default = False + if flag_value is None: + flag_value = not self.default + self.is_flag = is_flag + self.flag_value = flag_value + if self.is_flag and isinstance(self.flag_value, bool) \ + and type is None: + self.type = BOOL + self.is_bool_flag = True + else: + self.is_bool_flag = False + + # Counting + self.count = count + if count: + if type is None: + self.type = IntRange(min=0) + if default_is_missing: + self.default = 0 + + self.multiple = multiple + self.allow_from_autoenv = allow_from_autoenv + self.help = help + self.show_default = show_default + self.show_choices = show_choices + self.show_envvar = show_envvar + + # Sanity check for stuff we don't support + if __debug__: + if self.nargs < 0: + raise TypeError('Options cannot have nargs < 0') + if self.prompt and self.is_flag and not self.is_bool_flag: + raise TypeError('Cannot prompt for flags that are not bools.') + if not self.is_bool_flag and self.secondary_opts: + raise TypeError('Got secondary option for non boolean flag.') + if self.is_bool_flag and self.hide_input \ + and self.prompt is not None: + raise TypeError('Hidden input does not work with boolean ' + 'flag prompts.') + if self.count: + if self.multiple: + raise TypeError('Options cannot be multiple and count ' + 'at the same time.') + elif self.is_flag: + raise TypeError('Options cannot be count and flags at ' + 'the same time.') + + def _parse_decls(self, decls, expose_value): + opts = [] + secondary_opts = [] + name = None + possible_names = [] + + for decl in decls: + if isidentifier(decl): + if name is not None: + raise TypeError('Name defined twice') + name = decl + else: + split_char = decl[:1] == '/' and ';' or '/' + if split_char in decl: + first, second = decl.split(split_char, 1) + first = first.rstrip() + if first: + possible_names.append(split_opt(first)) + opts.append(first) + second = second.lstrip() + if second: + secondary_opts.append(second.lstrip()) + else: + possible_names.append(split_opt(decl)) + opts.append(decl) + + if name is None and possible_names: + possible_names.sort(key=lambda x: -len(x[0])) # group long options first + name = possible_names[0][1].replace('-', '_').lower() + if not isidentifier(name): + name = None + + if name is None: + if not expose_value: + return None, opts, secondary_opts + raise TypeError('Could not determine name for option') + + if not opts and not secondary_opts: + raise TypeError('No options defined but a name was passed (%s). ' + 'Did you mean to declare an argument instead ' + 'of an option?' % name) + + return name, opts, secondary_opts + + def add_to_parser(self, parser, ctx): + kwargs = { + 'dest': self.name, + 'nargs': self.nargs, + 'obj': self, + } + + if self.multiple: + action = 'append' + elif self.count: + action = 'count' + else: + action = 'store' + + if self.is_flag: + kwargs.pop('nargs', None) + if self.is_bool_flag and self.secondary_opts: + parser.add_option(self.opts, action=action + '_const', + const=True, **kwargs) + parser.add_option(self.secondary_opts, action=action + + '_const', const=False, **kwargs) + else: + parser.add_option(self.opts, action=action + '_const', + const=self.flag_value, + **kwargs) + else: + kwargs['action'] = action + parser.add_option(self.opts, **kwargs) + + def get_help_record(self, ctx): + if self.hidden: + return + any_prefix_is_slash = [] + + def _write_opts(opts): + rv, any_slashes = join_options(opts) + if any_slashes: + any_prefix_is_slash[:] = [True] + if not self.is_flag and not self.count: + rv += ' ' + self.make_metavar() + return rv + + rv = [_write_opts(self.opts)] + if self.secondary_opts: + rv.append(_write_opts(self.secondary_opts)) + + help = self.help or '' + extra = [] + if self.show_envvar: + envvar = self.envvar + if envvar is None: + if self.allow_from_autoenv and \ + ctx.auto_envvar_prefix is not None: + envvar = '%s_%s' % (ctx.auto_envvar_prefix, self.name.upper()) + if envvar is not None: + extra.append('env var: %s' % ( + ', '.join('%s' % d for d in envvar) + if isinstance(envvar, (list, tuple)) + else envvar, )) + if self.default is not None and self.show_default: + if isinstance(self.show_default, string_types): + default_string = '({})'.format(self.show_default) + elif isinstance(self.default, (list, tuple)): + default_string = ', '.join('%s' % d for d in self.default) + elif inspect.isfunction(self.default): + default_string = "(dynamic)" + else: + default_string = self.default + extra.append('default: {}'.format(default_string)) + + if self.required: + extra.append('required') + if extra: + help = '%s[%s]' % (help and help + ' ' or '', '; '.join(extra)) + + return ((any_prefix_is_slash and '; ' or ' / ').join(rv), help) + + def get_default(self, ctx): + # If we're a non boolean flag out default is more complex because + # we need to look at all flags in the same group to figure out + # if we're the the default one in which case we return the flag + # value as default. + if self.is_flag and not self.is_bool_flag: + for param in ctx.command.params: + if param.name == self.name and param.default: + return param.flag_value + return None + return Parameter.get_default(self, ctx) + + def prompt_for_value(self, ctx): + """This is an alternative flow that can be activated in the full + value processing if a value does not exist. It will prompt the + user until a valid value exists and then returns the processed + value as result. + """ + # Calculate the default before prompting anything to be stable. + default = self.get_default(ctx) + + # If this is a prompt for a flag we need to handle this + # differently. + if self.is_bool_flag: + return confirm(self.prompt, default) + + return prompt(self.prompt, default=default, type=self.type, + hide_input=self.hide_input, show_choices=self.show_choices, + confirmation_prompt=self.confirmation_prompt, + value_proc=lambda x: self.process_value(ctx, x)) + + def resolve_envvar_value(self, ctx): + rv = Parameter.resolve_envvar_value(self, ctx) + if rv is not None: + return rv + if self.allow_from_autoenv and \ + ctx.auto_envvar_prefix is not None: + envvar = '%s_%s' % (ctx.auto_envvar_prefix, self.name.upper()) + return os.environ.get(envvar) + + def value_from_envvar(self, ctx): + rv = self.resolve_envvar_value(ctx) + if rv is None: + return None + value_depth = (self.nargs != 1) + bool(self.multiple) + if value_depth > 0 and rv is not None: + rv = self.type.split_envvar_value(rv) + if self.multiple and self.nargs != 1: + rv = batch(rv, self.nargs) + return rv + + def full_process_value(self, ctx, value): + if value is None and self.prompt is not None \ + and not ctx.resilient_parsing: + return self.prompt_for_value(ctx) + return Parameter.full_process_value(self, ctx, value) + + +class Argument(Parameter): + """Arguments are positional parameters to a command. They generally + provide fewer features than options but can have infinite ``nargs`` + and are required by default. + + All parameters are passed onwards to the parameter constructor. + """ + param_type_name = 'argument' + + def __init__(self, param_decls, required=None, **attrs): + if required is None: + if attrs.get('default') is not None: + required = False + else: + required = attrs.get('nargs', 1) > 0 + Parameter.__init__(self, param_decls, required=required, **attrs) + if self.default is not None and self.nargs < 0: + raise TypeError('nargs=-1 in combination with a default value ' + 'is not supported.') + + @property + def human_readable_name(self): + if self.metavar is not None: + return self.metavar + return self.name.upper() + + def make_metavar(self): + if self.metavar is not None: + return self.metavar + var = self.type.get_metavar(self) + if not var: + var = self.name.upper() + if not self.required: + var = '[%s]' % var + if self.nargs != 1: + var += '...' + return var + + def _parse_decls(self, decls, expose_value): + if not decls: + if not expose_value: + return None, [], [] + raise TypeError('Could not determine name for argument') + if len(decls) == 1: + name = arg = decls[0] + name = name.replace('-', '_').lower() + else: + raise TypeError('Arguments take exactly one ' + 'parameter declaration, got %d' % len(decls)) + return name, [arg], [] + + def get_usage_pieces(self, ctx): + return [self.make_metavar()] + + def get_error_hint(self, ctx): + return '"%s"' % self.make_metavar() + + def add_to_parser(self, parser, ctx): + parser.add_argument(dest=self.name, nargs=self.nargs, + obj=self) + + +# Circular dependency between decorators and core +from .decorators import command, group diff --git a/libs/click/decorators.py b/libs/click/decorators.py new file mode 100644 index 00000000..c57c5308 --- /dev/null +++ b/libs/click/decorators.py @@ -0,0 +1,311 @@ +import sys +import inspect + +from functools import update_wrapper + +from ._compat import iteritems +from ._unicodefun import _check_for_unicode_literals +from .utils import echo +from .globals import get_current_context + + +def pass_context(f): + """Marks a callback as wanting to receive the current context + object as first argument. + """ + def new_func(*args, **kwargs): + return f(get_current_context(), *args, **kwargs) + return update_wrapper(new_func, f) + + +def pass_obj(f): + """Similar to :func:`pass_context`, but only pass the object on the + context onwards (:attr:`Context.obj`). This is useful if that object + represents the state of a nested system. + """ + def new_func(*args, **kwargs): + return f(get_current_context().obj, *args, **kwargs) + return update_wrapper(new_func, f) + + +def make_pass_decorator(object_type, ensure=False): + """Given an object type this creates a decorator that will work + similar to :func:`pass_obj` but instead of passing the object of the + current context, it will find the innermost context of type + :func:`object_type`. + + This generates a decorator that works roughly like this:: + + from functools import update_wrapper + + def decorator(f): + @pass_context + def new_func(ctx, *args, **kwargs): + obj = ctx.find_object(object_type) + return ctx.invoke(f, obj, *args, **kwargs) + return update_wrapper(new_func, f) + return decorator + + :param object_type: the type of the object to pass. + :param ensure: if set to `True`, a new object will be created and + remembered on the context if it's not there yet. + """ + def decorator(f): + def new_func(*args, **kwargs): + ctx = get_current_context() + if ensure: + obj = ctx.ensure_object(object_type) + else: + obj = ctx.find_object(object_type) + if obj is None: + raise RuntimeError('Managed to invoke callback without a ' + 'context object of type %r existing' + % object_type.__name__) + return ctx.invoke(f, obj, *args, **kwargs) + return update_wrapper(new_func, f) + return decorator + + +def _make_command(f, name, attrs, cls): + if isinstance(f, Command): + raise TypeError('Attempted to convert a callback into a ' + 'command twice.') + try: + params = f.__click_params__ + params.reverse() + del f.__click_params__ + except AttributeError: + params = [] + help = attrs.get('help') + if help is None: + help = inspect.getdoc(f) + if isinstance(help, bytes): + help = help.decode('utf-8') + else: + help = inspect.cleandoc(help) + attrs['help'] = help + _check_for_unicode_literals() + return cls(name=name or f.__name__.lower().replace('_', '-'), + callback=f, params=params, **attrs) + + +def command(name=None, cls=None, **attrs): + r"""Creates a new :class:`Command` and uses the decorated function as + callback. This will also automatically attach all decorated + :func:`option`\s and :func:`argument`\s as parameters to the command. + + The name of the command defaults to the name of the function. If you + want to change that, you can pass the intended name as the first + argument. + + All keyword arguments are forwarded to the underlying command class. + + Once decorated the function turns into a :class:`Command` instance + that can be invoked as a command line utility or be attached to a + command :class:`Group`. + + :param name: the name of the command. This defaults to the function + name with underscores replaced by dashes. + :param cls: the command class to instantiate. This defaults to + :class:`Command`. + """ + if cls is None: + cls = Command + def decorator(f): + cmd = _make_command(f, name, attrs, cls) + cmd.__doc__ = f.__doc__ + return cmd + return decorator + + +def group(name=None, **attrs): + """Creates a new :class:`Group` with a function as callback. This + works otherwise the same as :func:`command` just that the `cls` + parameter is set to :class:`Group`. + """ + attrs.setdefault('cls', Group) + return command(name, **attrs) + + +def _param_memo(f, param): + if isinstance(f, Command): + f.params.append(param) + else: + if not hasattr(f, '__click_params__'): + f.__click_params__ = [] + f.__click_params__.append(param) + + +def argument(*param_decls, **attrs): + """Attaches an argument to the command. All positional arguments are + passed as parameter declarations to :class:`Argument`; all keyword + arguments are forwarded unchanged (except ``cls``). + This is equivalent to creating an :class:`Argument` instance manually + and attaching it to the :attr:`Command.params` list. + + :param cls: the argument class to instantiate. This defaults to + :class:`Argument`. + """ + def decorator(f): + ArgumentClass = attrs.pop('cls', Argument) + _param_memo(f, ArgumentClass(param_decls, **attrs)) + return f + return decorator + + +def option(*param_decls, **attrs): + """Attaches an option to the command. All positional arguments are + passed as parameter declarations to :class:`Option`; all keyword + arguments are forwarded unchanged (except ``cls``). + This is equivalent to creating an :class:`Option` instance manually + and attaching it to the :attr:`Command.params` list. + + :param cls: the option class to instantiate. This defaults to + :class:`Option`. + """ + def decorator(f): + # Issue 926, copy attrs, so pre-defined options can re-use the same cls= + option_attrs = attrs.copy() + + if 'help' in option_attrs: + option_attrs['help'] = inspect.cleandoc(option_attrs['help']) + OptionClass = option_attrs.pop('cls', Option) + _param_memo(f, OptionClass(param_decls, **option_attrs)) + return f + return decorator + + +def confirmation_option(*param_decls, **attrs): + """Shortcut for confirmation prompts that can be ignored by passing + ``--yes`` as parameter. + + This is equivalent to decorating a function with :func:`option` with + the following parameters:: + + def callback(ctx, param, value): + if not value: + ctx.abort() + + @click.command() + @click.option('--yes', is_flag=True, callback=callback, + expose_value=False, prompt='Do you want to continue?') + def dropdb(): + pass + """ + def decorator(f): + def callback(ctx, param, value): + if not value: + ctx.abort() + attrs.setdefault('is_flag', True) + attrs.setdefault('callback', callback) + attrs.setdefault('expose_value', False) + attrs.setdefault('prompt', 'Do you want to continue?') + attrs.setdefault('help', 'Confirm the action without prompting.') + return option(*(param_decls or ('--yes',)), **attrs)(f) + return decorator + + +def password_option(*param_decls, **attrs): + """Shortcut for password prompts. + + This is equivalent to decorating a function with :func:`option` with + the following parameters:: + + @click.command() + @click.option('--password', prompt=True, confirmation_prompt=True, + hide_input=True) + def changeadmin(password): + pass + """ + def decorator(f): + attrs.setdefault('prompt', True) + attrs.setdefault('confirmation_prompt', True) + attrs.setdefault('hide_input', True) + return option(*(param_decls or ('--password',)), **attrs)(f) + return decorator + + +def version_option(version=None, *param_decls, **attrs): + """Adds a ``--version`` option which immediately ends the program + printing out the version number. This is implemented as an eager + option that prints the version and exits the program in the callback. + + :param version: the version number to show. If not provided Click + attempts an auto discovery via setuptools. + :param prog_name: the name of the program (defaults to autodetection) + :param message: custom message to show instead of the default + (``'%(prog)s, version %(version)s'``) + :param others: everything else is forwarded to :func:`option`. + """ + if version is None: + if hasattr(sys, '_getframe'): + module = sys._getframe(1).f_globals.get('__name__') + else: + module = '' + + def decorator(f): + prog_name = attrs.pop('prog_name', None) + message = attrs.pop('message', '%(prog)s, version %(version)s') + + def callback(ctx, param, value): + if not value or ctx.resilient_parsing: + return + prog = prog_name + if prog is None: + prog = ctx.find_root().info_name + ver = version + if ver is None: + try: + import pkg_resources + except ImportError: + pass + else: + for dist in pkg_resources.working_set: + scripts = dist.get_entry_map().get('console_scripts') or {} + for script_name, entry_point in iteritems(scripts): + if entry_point.module_name == module: + ver = dist.version + break + if ver is None: + raise RuntimeError('Could not determine version') + echo(message % { + 'prog': prog, + 'version': ver, + }, color=ctx.color) + ctx.exit() + + attrs.setdefault('is_flag', True) + attrs.setdefault('expose_value', False) + attrs.setdefault('is_eager', True) + attrs.setdefault('help', 'Show the version and exit.') + attrs['callback'] = callback + return option(*(param_decls or ('--version',)), **attrs)(f) + return decorator + + +def help_option(*param_decls, **attrs): + """Adds a ``--help`` option which immediately ends the program + printing out the help page. This is usually unnecessary to add as + this is added by default to all commands unless suppressed. + + Like :func:`version_option`, this is implemented as eager option that + prints in the callback and exits. + + All arguments are forwarded to :func:`option`. + """ + def decorator(f): + def callback(ctx, param, value): + if value and not ctx.resilient_parsing: + echo(ctx.get_help(), color=ctx.color) + ctx.exit() + attrs.setdefault('is_flag', True) + attrs.setdefault('expose_value', False) + attrs.setdefault('help', 'Show this message and exit.') + attrs.setdefault('is_eager', True) + attrs['callback'] = callback + return option(*(param_decls or ('--help',)), **attrs)(f) + return decorator + + +# Circular dependencies between core and decorators +from .core import Command, Group, Argument, Option diff --git a/libs/click/exceptions.py b/libs/click/exceptions.py new file mode 100644 index 00000000..6fa17658 --- /dev/null +++ b/libs/click/exceptions.py @@ -0,0 +1,235 @@ +from ._compat import PY2, filename_to_ui, get_text_stderr +from .utils import echo + + +def _join_param_hints(param_hint): + if isinstance(param_hint, (tuple, list)): + return ' / '.join('"%s"' % x for x in param_hint) + return param_hint + + +class ClickException(Exception): + """An exception that Click can handle and show to the user.""" + + #: The exit code for this exception + exit_code = 1 + + def __init__(self, message): + ctor_msg = message + if PY2: + if ctor_msg is not None: + ctor_msg = ctor_msg.encode('utf-8') + Exception.__init__(self, ctor_msg) + self.message = message + + def format_message(self): + return self.message + + def __str__(self): + return self.message + + if PY2: + __unicode__ = __str__ + + def __str__(self): + return self.message.encode('utf-8') + + def show(self, file=None): + if file is None: + file = get_text_stderr() + echo('Error: %s' % self.format_message(), file=file) + + +class UsageError(ClickException): + """An internal exception that signals a usage error. This typically + aborts any further handling. + + :param message: the error message to display. + :param ctx: optionally the context that caused this error. Click will + fill in the context automatically in some situations. + """ + exit_code = 2 + + def __init__(self, message, ctx=None): + ClickException.__init__(self, message) + self.ctx = ctx + self.cmd = self.ctx and self.ctx.command or None + + def show(self, file=None): + if file is None: + file = get_text_stderr() + color = None + hint = '' + if (self.cmd is not None and + self.cmd.get_help_option(self.ctx) is not None): + hint = ('Try "%s %s" for help.\n' + % (self.ctx.command_path, self.ctx.help_option_names[0])) + if self.ctx is not None: + color = self.ctx.color + echo(self.ctx.get_usage() + '\n%s' % hint, file=file, color=color) + echo('Error: %s' % self.format_message(), file=file, color=color) + + +class BadParameter(UsageError): + """An exception that formats out a standardized error message for a + bad parameter. This is useful when thrown from a callback or type as + Click will attach contextual information to it (for instance, which + parameter it is). + + .. versionadded:: 2.0 + + :param param: the parameter object that caused this error. This can + be left out, and Click will attach this info itself + if possible. + :param param_hint: a string that shows up as parameter name. This + can be used as alternative to `param` in cases + where custom validation should happen. If it is + a string it's used as such, if it's a list then + each item is quoted and separated. + """ + + def __init__(self, message, ctx=None, param=None, + param_hint=None): + UsageError.__init__(self, message, ctx) + self.param = param + self.param_hint = param_hint + + def format_message(self): + if self.param_hint is not None: + param_hint = self.param_hint + elif self.param is not None: + param_hint = self.param.get_error_hint(self.ctx) + else: + return 'Invalid value: %s' % self.message + param_hint = _join_param_hints(param_hint) + + return 'Invalid value for %s: %s' % (param_hint, self.message) + + +class MissingParameter(BadParameter): + """Raised if click required an option or argument but it was not + provided when invoking the script. + + .. versionadded:: 4.0 + + :param param_type: a string that indicates the type of the parameter. + The default is to inherit the parameter type from + the given `param`. Valid values are ``'parameter'``, + ``'option'`` or ``'argument'``. + """ + + def __init__(self, message=None, ctx=None, param=None, + param_hint=None, param_type=None): + BadParameter.__init__(self, message, ctx, param, param_hint) + self.param_type = param_type + + def format_message(self): + if self.param_hint is not None: + param_hint = self.param_hint + elif self.param is not None: + param_hint = self.param.get_error_hint(self.ctx) + else: + param_hint = None + param_hint = _join_param_hints(param_hint) + + param_type = self.param_type + if param_type is None and self.param is not None: + param_type = self.param.param_type_name + + msg = self.message + if self.param is not None: + msg_extra = self.param.type.get_missing_message(self.param) + if msg_extra: + if msg: + msg += '. ' + msg_extra + else: + msg = msg_extra + + return 'Missing %s%s%s%s' % ( + param_type, + param_hint and ' %s' % param_hint or '', + msg and '. ' or '.', + msg or '', + ) + + +class NoSuchOption(UsageError): + """Raised if click attempted to handle an option that does not + exist. + + .. versionadded:: 4.0 + """ + + def __init__(self, option_name, message=None, possibilities=None, + ctx=None): + if message is None: + message = 'no such option: %s' % option_name + UsageError.__init__(self, message, ctx) + self.option_name = option_name + self.possibilities = possibilities + + def format_message(self): + bits = [self.message] + if self.possibilities: + if len(self.possibilities) == 1: + bits.append('Did you mean %s?' % self.possibilities[0]) + else: + possibilities = sorted(self.possibilities) + bits.append('(Possible options: %s)' % ', '.join(possibilities)) + return ' '.join(bits) + + +class BadOptionUsage(UsageError): + """Raised if an option is generally supplied but the use of the option + was incorrect. This is for instance raised if the number of arguments + for an option is not correct. + + .. versionadded:: 4.0 + + :param option_name: the name of the option being used incorrectly. + """ + + def __init__(self, option_name, message, ctx=None): + UsageError.__init__(self, message, ctx) + self.option_name = option_name + + +class BadArgumentUsage(UsageError): + """Raised if an argument is generally supplied but the use of the argument + was incorrect. This is for instance raised if the number of values + for an argument is not correct. + + .. versionadded:: 6.0 + """ + + def __init__(self, message, ctx=None): + UsageError.__init__(self, message, ctx) + + +class FileError(ClickException): + """Raised if a file cannot be opened.""" + + def __init__(self, filename, hint=None): + ui_filename = filename_to_ui(filename) + if hint is None: + hint = 'unknown error' + ClickException.__init__(self, hint) + self.ui_filename = ui_filename + self.filename = filename + + def format_message(self): + return 'Could not open file %s: %s' % (self.ui_filename, self.message) + + +class Abort(RuntimeError): + """An internal signalling exception that signals Click to abort.""" + + +class Exit(RuntimeError): + """An exception that indicates that the application should exit with some + status code. + + :param code: the status code to exit with. + """ + def __init__(self, code=0): + self.exit_code = code diff --git a/libs/click/formatting.py b/libs/click/formatting.py new file mode 100644 index 00000000..a3d6a4d3 --- /dev/null +++ b/libs/click/formatting.py @@ -0,0 +1,256 @@ +from contextlib import contextmanager +from .termui import get_terminal_size +from .parser import split_opt +from ._compat import term_len + + +# Can force a width. This is used by the test system +FORCED_WIDTH = None + + +def measure_table(rows): + widths = {} + for row in rows: + for idx, col in enumerate(row): + widths[idx] = max(widths.get(idx, 0), term_len(col)) + return tuple(y for x, y in sorted(widths.items())) + + +def iter_rows(rows, col_count): + for row in rows: + row = tuple(row) + yield row + ('',) * (col_count - len(row)) + + +def wrap_text(text, width=78, initial_indent='', subsequent_indent='', + preserve_paragraphs=False): + """A helper function that intelligently wraps text. By default, it + assumes that it operates on a single paragraph of text but if the + `preserve_paragraphs` parameter is provided it will intelligently + handle paragraphs (defined by two empty lines). + + If paragraphs are handled, a paragraph can be prefixed with an empty + line containing the ``\\b`` character (``\\x08``) to indicate that + no rewrapping should happen in that block. + + :param text: the text that should be rewrapped. + :param width: the maximum width for the text. + :param initial_indent: the initial indent that should be placed on the + first line as a string. + :param subsequent_indent: the indent string that should be placed on + each consecutive line. + :param preserve_paragraphs: if this flag is set then the wrapping will + intelligently handle paragraphs. + """ + from ._textwrap import TextWrapper + text = text.expandtabs() + wrapper = TextWrapper(width, initial_indent=initial_indent, + subsequent_indent=subsequent_indent, + replace_whitespace=False) + if not preserve_paragraphs: + return wrapper.fill(text) + + p = [] + buf = [] + indent = None + + def _flush_par(): + if not buf: + return + if buf[0].strip() == '\b': + p.append((indent or 0, True, '\n'.join(buf[1:]))) + else: + p.append((indent or 0, False, ' '.join(buf))) + del buf[:] + + for line in text.splitlines(): + if not line: + _flush_par() + indent = None + else: + if indent is None: + orig_len = term_len(line) + line = line.lstrip() + indent = orig_len - term_len(line) + buf.append(line) + _flush_par() + + rv = [] + for indent, raw, text in p: + with wrapper.extra_indent(' ' * indent): + if raw: + rv.append(wrapper.indent_only(text)) + else: + rv.append(wrapper.fill(text)) + + return '\n\n'.join(rv) + + +class HelpFormatter(object): + """This class helps with formatting text-based help pages. It's + usually just needed for very special internal cases, but it's also + exposed so that developers can write their own fancy outputs. + + At present, it always writes into memory. + + :param indent_increment: the additional increment for each level. + :param width: the width for the text. This defaults to the terminal + width clamped to a maximum of 78. + """ + + def __init__(self, indent_increment=2, width=None, max_width=None): + self.indent_increment = indent_increment + if max_width is None: + max_width = 80 + if width is None: + width = FORCED_WIDTH + if width is None: + width = max(min(get_terminal_size()[0], max_width) - 2, 50) + self.width = width + self.current_indent = 0 + self.buffer = [] + + def write(self, string): + """Writes a unicode string into the internal buffer.""" + self.buffer.append(string) + + def indent(self): + """Increases the indentation.""" + self.current_indent += self.indent_increment + + def dedent(self): + """Decreases the indentation.""" + self.current_indent -= self.indent_increment + + def write_usage(self, prog, args='', prefix='Usage: '): + """Writes a usage line into the buffer. + + :param prog: the program name. + :param args: whitespace separated list of arguments. + :param prefix: the prefix for the first line. + """ + usage_prefix = '%*s%s ' % (self.current_indent, prefix, prog) + text_width = self.width - self.current_indent + + if text_width >= (term_len(usage_prefix) + 20): + # The arguments will fit to the right of the prefix. + indent = ' ' * term_len(usage_prefix) + self.write(wrap_text(args, text_width, + initial_indent=usage_prefix, + subsequent_indent=indent)) + else: + # The prefix is too long, put the arguments on the next line. + self.write(usage_prefix) + self.write('\n') + indent = ' ' * (max(self.current_indent, term_len(prefix)) + 4) + self.write(wrap_text(args, text_width, + initial_indent=indent, + subsequent_indent=indent)) + + self.write('\n') + + def write_heading(self, heading): + """Writes a heading into the buffer.""" + self.write('%*s%s:\n' % (self.current_indent, '', heading)) + + def write_paragraph(self): + """Writes a paragraph into the buffer.""" + if self.buffer: + self.write('\n') + + def write_text(self, text): + """Writes re-indented text into the buffer. This rewraps and + preserves paragraphs. + """ + text_width = max(self.width - self.current_indent, 11) + indent = ' ' * self.current_indent + self.write(wrap_text(text, text_width, + initial_indent=indent, + subsequent_indent=indent, + preserve_paragraphs=True)) + self.write('\n') + + def write_dl(self, rows, col_max=30, col_spacing=2): + """Writes a definition list into the buffer. This is how options + and commands are usually formatted. + + :param rows: a list of two item tuples for the terms and values. + :param col_max: the maximum width of the first column. + :param col_spacing: the number of spaces between the first and + second column. + """ + rows = list(rows) + widths = measure_table(rows) + if len(widths) != 2: + raise TypeError('Expected two columns for definition list') + + first_col = min(widths[0], col_max) + col_spacing + + for first, second in iter_rows(rows, len(widths)): + self.write('%*s%s' % (self.current_indent, '', first)) + if not second: + self.write('\n') + continue + if term_len(first) <= first_col - col_spacing: + self.write(' ' * (first_col - term_len(first))) + else: + self.write('\n') + self.write(' ' * (first_col + self.current_indent)) + + text_width = max(self.width - first_col - 2, 10) + lines = iter(wrap_text(second, text_width).splitlines()) + if lines: + self.write(next(lines) + '\n') + for line in lines: + self.write('%*s%s\n' % ( + first_col + self.current_indent, '', line)) + else: + self.write('\n') + + @contextmanager + def section(self, name): + """Helpful context manager that writes a paragraph, a heading, + and the indents. + + :param name: the section name that is written as heading. + """ + self.write_paragraph() + self.write_heading(name) + self.indent() + try: + yield + finally: + self.dedent() + + @contextmanager + def indentation(self): + """A context manager that increases the indentation.""" + self.indent() + try: + yield + finally: + self.dedent() + + def getvalue(self): + """Returns the buffer contents.""" + return ''.join(self.buffer) + + +def join_options(options): + """Given a list of option strings this joins them in the most appropriate + way and returns them in the form ``(formatted_string, + any_prefix_is_slash)`` where the second item in the tuple is a flag that + indicates if any of the option prefixes was a slash. + """ + rv = [] + any_prefix_is_slash = False + for opt in options: + prefix = split_opt(opt)[0] + if prefix == '/': + any_prefix_is_slash = True + rv.append((len(prefix), opt)) + + rv.sort(key=lambda x: x[0]) + + rv = ', '.join(x[1] for x in rv) + return rv, any_prefix_is_slash diff --git a/libs/click/globals.py b/libs/click/globals.py new file mode 100644 index 00000000..843b594a --- /dev/null +++ b/libs/click/globals.py @@ -0,0 +1,48 @@ +from threading import local + + +_local = local() + + +def get_current_context(silent=False): + """Returns the current click context. This can be used as a way to + access the current context object from anywhere. This is a more implicit + alternative to the :func:`pass_context` decorator. This function is + primarily useful for helpers such as :func:`echo` which might be + interested in changing its behavior based on the current context. + + To push the current context, :meth:`Context.scope` can be used. + + .. versionadded:: 5.0 + + :param silent: is set to `True` the return value is `None` if no context + is available. The default behavior is to raise a + :exc:`RuntimeError`. + """ + try: + return getattr(_local, 'stack')[-1] + except (AttributeError, IndexError): + if not silent: + raise RuntimeError('There is no active click context.') + + +def push_context(ctx): + """Pushes a new context to the current stack.""" + _local.__dict__.setdefault('stack', []).append(ctx) + + +def pop_context(): + """Removes the top level from the stack.""" + _local.stack.pop() + + +def resolve_color_default(color=None): + """"Internal helper to get the default value of the color flag. If a + value is passed it's returned unchanged, otherwise it's looked up from + the current context. + """ + if color is not None: + return color + ctx = get_current_context(silent=True) + if ctx is not None: + return ctx.color diff --git a/libs/click/parser.py b/libs/click/parser.py new file mode 100644 index 00000000..1c3ae9c8 --- /dev/null +++ b/libs/click/parser.py @@ -0,0 +1,427 @@ +# -*- coding: utf-8 -*- +""" +click.parser +~~~~~~~~~~~~ + +This module started out as largely a copy paste from the stdlib's +optparse module with the features removed that we do not need from +optparse because we implement them in Click on a higher level (for +instance type handling, help formatting and a lot more). + +The plan is to remove more and more from here over time. + +The reason this is a different module and not optparse from the stdlib +is that there are differences in 2.x and 3.x about the error messages +generated and optparse in the stdlib uses gettext for no good reason +and might cause us issues. +""" + +import re +from collections import deque +from .exceptions import UsageError, NoSuchOption, BadOptionUsage, \ + BadArgumentUsage + + +def _unpack_args(args, nargs_spec): + """Given an iterable of arguments and an iterable of nargs specifications, + it returns a tuple with all the unpacked arguments at the first index + and all remaining arguments as the second. + + The nargs specification is the number of arguments that should be consumed + or `-1` to indicate that this position should eat up all the remainders. + + Missing items are filled with `None`. + """ + args = deque(args) + nargs_spec = deque(nargs_spec) + rv = [] + spos = None + + def _fetch(c): + try: + if spos is None: + return c.popleft() + else: + return c.pop() + except IndexError: + return None + + while nargs_spec: + nargs = _fetch(nargs_spec) + if nargs == 1: + rv.append(_fetch(args)) + elif nargs > 1: + x = [_fetch(args) for _ in range(nargs)] + # If we're reversed, we're pulling in the arguments in reverse, + # so we need to turn them around. + if spos is not None: + x.reverse() + rv.append(tuple(x)) + elif nargs < 0: + if spos is not None: + raise TypeError('Cannot have two nargs < 0') + spos = len(rv) + rv.append(None) + + # spos is the position of the wildcard (star). If it's not `None`, + # we fill it with the remainder. + if spos is not None: + rv[spos] = tuple(args) + args = [] + rv[spos + 1:] = reversed(rv[spos + 1:]) + + return tuple(rv), list(args) + + +def _error_opt_args(nargs, opt): + if nargs == 1: + raise BadOptionUsage(opt, '%s option requires an argument' % opt) + raise BadOptionUsage(opt, '%s option requires %d arguments' % (opt, nargs)) + + +def split_opt(opt): + first = opt[:1] + if first.isalnum(): + return '', opt + if opt[1:2] == first: + return opt[:2], opt[2:] + return first, opt[1:] + + +def normalize_opt(opt, ctx): + if ctx is None or ctx.token_normalize_func is None: + return opt + prefix, opt = split_opt(opt) + return prefix + ctx.token_normalize_func(opt) + + +def split_arg_string(string): + """Given an argument string this attempts to split it into small parts.""" + rv = [] + for match in re.finditer(r"('([^'\\]*(?:\\.[^'\\]*)*)'" + r'|"([^"\\]*(?:\\.[^"\\]*)*)"' + r'|\S+)\s*', string, re.S): + arg = match.group().strip() + if arg[:1] == arg[-1:] and arg[:1] in '"\'': + arg = arg[1:-1].encode('ascii', 'backslashreplace') \ + .decode('unicode-escape') + try: + arg = type(string)(arg) + except UnicodeError: + pass + rv.append(arg) + return rv + + +class Option(object): + + def __init__(self, opts, dest, action=None, nargs=1, const=None, obj=None): + self._short_opts = [] + self._long_opts = [] + self.prefixes = set() + + for opt in opts: + prefix, value = split_opt(opt) + if not prefix: + raise ValueError('Invalid start character for option (%s)' + % opt) + self.prefixes.add(prefix[0]) + if len(prefix) == 1 and len(value) == 1: + self._short_opts.append(opt) + else: + self._long_opts.append(opt) + self.prefixes.add(prefix) + + if action is None: + action = 'store' + + self.dest = dest + self.action = action + self.nargs = nargs + self.const = const + self.obj = obj + + @property + def takes_value(self): + return self.action in ('store', 'append') + + def process(self, value, state): + if self.action == 'store': + state.opts[self.dest] = value + elif self.action == 'store_const': + state.opts[self.dest] = self.const + elif self.action == 'append': + state.opts.setdefault(self.dest, []).append(value) + elif self.action == 'append_const': + state.opts.setdefault(self.dest, []).append(self.const) + elif self.action == 'count': + state.opts[self.dest] = state.opts.get(self.dest, 0) + 1 + else: + raise ValueError('unknown action %r' % self.action) + state.order.append(self.obj) + + +class Argument(object): + + def __init__(self, dest, nargs=1, obj=None): + self.dest = dest + self.nargs = nargs + self.obj = obj + + def process(self, value, state): + if self.nargs > 1: + holes = sum(1 for x in value if x is None) + if holes == len(value): + value = None + elif holes != 0: + raise BadArgumentUsage('argument %s takes %d values' + % (self.dest, self.nargs)) + state.opts[self.dest] = value + state.order.append(self.obj) + + +class ParsingState(object): + + def __init__(self, rargs): + self.opts = {} + self.largs = [] + self.rargs = rargs + self.order = [] + + +class OptionParser(object): + """The option parser is an internal class that is ultimately used to + parse options and arguments. It's modelled after optparse and brings + a similar but vastly simplified API. It should generally not be used + directly as the high level Click classes wrap it for you. + + It's not nearly as extensible as optparse or argparse as it does not + implement features that are implemented on a higher level (such as + types or defaults). + + :param ctx: optionally the :class:`~click.Context` where this parser + should go with. + """ + + def __init__(self, ctx=None): + #: The :class:`~click.Context` for this parser. This might be + #: `None` for some advanced use cases. + self.ctx = ctx + #: This controls how the parser deals with interspersed arguments. + #: If this is set to `False`, the parser will stop on the first + #: non-option. Click uses this to implement nested subcommands + #: safely. + self.allow_interspersed_args = True + #: This tells the parser how to deal with unknown options. By + #: default it will error out (which is sensible), but there is a + #: second mode where it will ignore it and continue processing + #: after shifting all the unknown options into the resulting args. + self.ignore_unknown_options = False + if ctx is not None: + self.allow_interspersed_args = ctx.allow_interspersed_args + self.ignore_unknown_options = ctx.ignore_unknown_options + self._short_opt = {} + self._long_opt = {} + self._opt_prefixes = set(['-', '--']) + self._args = [] + + def add_option(self, opts, dest, action=None, nargs=1, const=None, + obj=None): + """Adds a new option named `dest` to the parser. The destination + is not inferred (unlike with optparse) and needs to be explicitly + provided. Action can be any of ``store``, ``store_const``, + ``append``, ``appnd_const`` or ``count``. + + The `obj` can be used to identify the option in the order list + that is returned from the parser. + """ + if obj is None: + obj = dest + opts = [normalize_opt(opt, self.ctx) for opt in opts] + option = Option(opts, dest, action=action, nargs=nargs, + const=const, obj=obj) + self._opt_prefixes.update(option.prefixes) + for opt in option._short_opts: + self._short_opt[opt] = option + for opt in option._long_opts: + self._long_opt[opt] = option + + def add_argument(self, dest, nargs=1, obj=None): + """Adds a positional argument named `dest` to the parser. + + The `obj` can be used to identify the option in the order list + that is returned from the parser. + """ + if obj is None: + obj = dest + self._args.append(Argument(dest=dest, nargs=nargs, obj=obj)) + + def parse_args(self, args): + """Parses positional arguments and returns ``(values, args, order)`` + for the parsed options and arguments as well as the leftover + arguments if there are any. The order is a list of objects as they + appear on the command line. If arguments appear multiple times they + will be memorized multiple times as well. + """ + state = ParsingState(args) + try: + self._process_args_for_options(state) + self._process_args_for_args(state) + except UsageError: + if self.ctx is None or not self.ctx.resilient_parsing: + raise + return state.opts, state.largs, state.order + + def _process_args_for_args(self, state): + pargs, args = _unpack_args(state.largs + state.rargs, + [x.nargs for x in self._args]) + + for idx, arg in enumerate(self._args): + arg.process(pargs[idx], state) + + state.largs = args + state.rargs = [] + + def _process_args_for_options(self, state): + while state.rargs: + arg = state.rargs.pop(0) + arglen = len(arg) + # Double dashes always handled explicitly regardless of what + # prefixes are valid. + if arg == '--': + return + elif arg[:1] in self._opt_prefixes and arglen > 1: + self._process_opts(arg, state) + elif self.allow_interspersed_args: + state.largs.append(arg) + else: + state.rargs.insert(0, arg) + return + + # Say this is the original argument list: + # [arg0, arg1, ..., arg(i-1), arg(i), arg(i+1), ..., arg(N-1)] + # ^ + # (we are about to process arg(i)). + # + # Then rargs is [arg(i), ..., arg(N-1)] and largs is a *subset* of + # [arg0, ..., arg(i-1)] (any options and their arguments will have + # been removed from largs). + # + # The while loop will usually consume 1 or more arguments per pass. + # If it consumes 1 (eg. arg is an option that takes no arguments), + # then after _process_arg() is done the situation is: + # + # largs = subset of [arg0, ..., arg(i)] + # rargs = [arg(i+1), ..., arg(N-1)] + # + # If allow_interspersed_args is false, largs will always be + # *empty* -- still a subset of [arg0, ..., arg(i-1)], but + # not a very interesting subset! + + def _match_long_opt(self, opt, explicit_value, state): + if opt not in self._long_opt: + possibilities = [word for word in self._long_opt + if word.startswith(opt)] + raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx) + + option = self._long_opt[opt] + if option.takes_value: + # At this point it's safe to modify rargs by injecting the + # explicit value, because no exception is raised in this + # branch. This means that the inserted value will be fully + # consumed. + if explicit_value is not None: + state.rargs.insert(0, explicit_value) + + nargs = option.nargs + if len(state.rargs) < nargs: + _error_opt_args(nargs, opt) + elif nargs == 1: + value = state.rargs.pop(0) + else: + value = tuple(state.rargs[:nargs]) + del state.rargs[:nargs] + + elif explicit_value is not None: + raise BadOptionUsage(opt, '%s option does not take a value' % opt) + + else: + value = None + + option.process(value, state) + + def _match_short_opt(self, arg, state): + stop = False + i = 1 + prefix = arg[0] + unknown_options = [] + + for ch in arg[1:]: + opt = normalize_opt(prefix + ch, self.ctx) + option = self._short_opt.get(opt) + i += 1 + + if not option: + if self.ignore_unknown_options: + unknown_options.append(ch) + continue + raise NoSuchOption(opt, ctx=self.ctx) + if option.takes_value: + # Any characters left in arg? Pretend they're the + # next arg, and stop consuming characters of arg. + if i < len(arg): + state.rargs.insert(0, arg[i:]) + stop = True + + nargs = option.nargs + if len(state.rargs) < nargs: + _error_opt_args(nargs, opt) + elif nargs == 1: + value = state.rargs.pop(0) + else: + value = tuple(state.rargs[:nargs]) + del state.rargs[:nargs] + + else: + value = None + + option.process(value, state) + + if stop: + break + + # If we got any unknown options we re-combinate the string of the + # remaining options and re-attach the prefix, then report that + # to the state as new larg. This way there is basic combinatorics + # that can be achieved while still ignoring unknown arguments. + if self.ignore_unknown_options and unknown_options: + state.largs.append(prefix + ''.join(unknown_options)) + + def _process_opts(self, arg, state): + explicit_value = None + # Long option handling happens in two parts. The first part is + # supporting explicitly attached values. In any case, we will try + # to long match the option first. + if '=' in arg: + long_opt, explicit_value = arg.split('=', 1) + else: + long_opt = arg + norm_long_opt = normalize_opt(long_opt, self.ctx) + + # At this point we will match the (assumed) long option through + # the long option matching code. Note that this allows options + # like "-foo" to be matched as long options. + try: + self._match_long_opt(norm_long_opt, explicit_value, state) + except NoSuchOption: + # At this point the long option matching failed, and we need + # to try with short options. However there is a special rule + # which says, that if we have a two character options prefix + # (applies to "--foo" for instance), we do not dispatch to the + # short option code and will instead raise the no option + # error. + if arg[:2] not in self._opt_prefixes: + return self._match_short_opt(arg, state) + if not self.ignore_unknown_options: + raise + state.largs.append(arg) diff --git a/libs/click/termui.py b/libs/click/termui.py new file mode 100644 index 00000000..bf9a3aa1 --- /dev/null +++ b/libs/click/termui.py @@ -0,0 +1,606 @@ +import os +import sys +import struct +import inspect +import itertools + +from ._compat import raw_input, text_type, string_types, \ + isatty, strip_ansi, get_winterm_size, DEFAULT_COLUMNS, WIN +from .utils import echo +from .exceptions import Abort, UsageError +from .types import convert_type, Choice, Path +from .globals import resolve_color_default + + +# The prompt functions to use. The doc tools currently override these +# functions to customize how they work. +visible_prompt_func = raw_input + +_ansi_colors = { + 'black': 30, + 'red': 31, + 'green': 32, + 'yellow': 33, + 'blue': 34, + 'magenta': 35, + 'cyan': 36, + 'white': 37, + 'reset': 39, + 'bright_black': 90, + 'bright_red': 91, + 'bright_green': 92, + 'bright_yellow': 93, + 'bright_blue': 94, + 'bright_magenta': 95, + 'bright_cyan': 96, + 'bright_white': 97, +} +_ansi_reset_all = '\033[0m' + + +def hidden_prompt_func(prompt): + import getpass + return getpass.getpass(prompt) + + +def _build_prompt(text, suffix, show_default=False, default=None, show_choices=True, type=None): + prompt = text + if type is not None and show_choices and isinstance(type, Choice): + prompt += ' (' + ", ".join(map(str, type.choices)) + ')' + if default is not None and show_default: + prompt = '%s [%s]' % (prompt, default) + return prompt + suffix + + +def prompt(text, default=None, hide_input=False, confirmation_prompt=False, + type=None, value_proc=None, prompt_suffix=': ', show_default=True, + err=False, show_choices=True): + """Prompts a user for input. This is a convenience function that can + be used to prompt a user for input later. + + If the user aborts the input by sending a interrupt signal, this + function will catch it and raise a :exc:`Abort` exception. + + .. versionadded:: 7.0 + Added the show_choices parameter. + + .. versionadded:: 6.0 + Added unicode support for cmd.exe on Windows. + + .. versionadded:: 4.0 + Added the `err` parameter. + + :param text: the text to show for the prompt. + :param default: the default value to use if no input happens. If this + is not given it will prompt until it's aborted. + :param hide_input: if this is set to true then the input value will + be hidden. + :param confirmation_prompt: asks for confirmation for the value. + :param type: the type to use to check the value against. + :param value_proc: if this parameter is provided it's a function that + is invoked instead of the type conversion to + convert a value. + :param prompt_suffix: a suffix that should be added to the prompt. + :param show_default: shows or hides the default value in the prompt. + :param err: if set to true the file defaults to ``stderr`` instead of + ``stdout``, the same as with echo. + :param show_choices: Show or hide choices if the passed type is a Choice. + For example if type is a Choice of either day or week, + show_choices is true and text is "Group by" then the + prompt will be "Group by (day, week): ". + """ + result = None + + def prompt_func(text): + f = hide_input and hidden_prompt_func or visible_prompt_func + try: + # Write the prompt separately so that we get nice + # coloring through colorama on Windows + echo(text, nl=False, err=err) + return f('') + except (KeyboardInterrupt, EOFError): + # getpass doesn't print a newline if the user aborts input with ^C. + # Allegedly this behavior is inherited from getpass(3). + # A doc bug has been filed at https://bugs.python.org/issue24711 + if hide_input: + echo(None, err=err) + raise Abort() + + if value_proc is None: + value_proc = convert_type(type, default) + + prompt = _build_prompt(text, prompt_suffix, show_default, default, show_choices, type) + + while 1: + while 1: + value = prompt_func(prompt) + if value: + break + elif default is not None: + if isinstance(value_proc, Path): + # validate Path default value(exists, dir_okay etc.) + value = default + break + return default + try: + result = value_proc(value) + except UsageError as e: + echo('Error: %s' % e.message, err=err) + continue + if not confirmation_prompt: + return result + while 1: + value2 = prompt_func('Repeat for confirmation: ') + if value2: + break + if value == value2: + return result + echo('Error: the two entered values do not match', err=err) + + +def confirm(text, default=False, abort=False, prompt_suffix=': ', + show_default=True, err=False): + """Prompts for confirmation (yes/no question). + + If the user aborts the input by sending a interrupt signal this + function will catch it and raise a :exc:`Abort` exception. + + .. versionadded:: 4.0 + Added the `err` parameter. + + :param text: the question to ask. + :param default: the default for the prompt. + :param abort: if this is set to `True` a negative answer aborts the + exception by raising :exc:`Abort`. + :param prompt_suffix: a suffix that should be added to the prompt. + :param show_default: shows or hides the default value in the prompt. + :param err: if set to true the file defaults to ``stderr`` instead of + ``stdout``, the same as with echo. + """ + prompt = _build_prompt(text, prompt_suffix, show_default, + default and 'Y/n' or 'y/N') + while 1: + try: + # Write the prompt separately so that we get nice + # coloring through colorama on Windows + echo(prompt, nl=False, err=err) + value = visible_prompt_func('').lower().strip() + except (KeyboardInterrupt, EOFError): + raise Abort() + if value in ('y', 'yes'): + rv = True + elif value in ('n', 'no'): + rv = False + elif value == '': + rv = default + else: + echo('Error: invalid input', err=err) + continue + break + if abort and not rv: + raise Abort() + return rv + + +def get_terminal_size(): + """Returns the current size of the terminal as tuple in the form + ``(width, height)`` in columns and rows. + """ + # If shutil has get_terminal_size() (Python 3.3 and later) use that + if sys.version_info >= (3, 3): + import shutil + shutil_get_terminal_size = getattr(shutil, 'get_terminal_size', None) + if shutil_get_terminal_size: + sz = shutil_get_terminal_size() + return sz.columns, sz.lines + + # We provide a sensible default for get_winterm_size() when being invoked + # inside a subprocess. Without this, it would not provide a useful input. + if get_winterm_size is not None: + size = get_winterm_size() + if size == (0, 0): + return (79, 24) + else: + return size + + def ioctl_gwinsz(fd): + try: + import fcntl + import termios + cr = struct.unpack( + 'hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')) + except Exception: + return + return cr + + cr = ioctl_gwinsz(0) or ioctl_gwinsz(1) or ioctl_gwinsz(2) + if not cr: + try: + fd = os.open(os.ctermid(), os.O_RDONLY) + try: + cr = ioctl_gwinsz(fd) + finally: + os.close(fd) + except Exception: + pass + if not cr or not cr[0] or not cr[1]: + cr = (os.environ.get('LINES', 25), + os.environ.get('COLUMNS', DEFAULT_COLUMNS)) + return int(cr[1]), int(cr[0]) + + +def echo_via_pager(text_or_generator, color=None): + """This function takes a text and shows it via an environment specific + pager on stdout. + + .. versionchanged:: 3.0 + Added the `color` flag. + + :param text_or_generator: the text to page, or alternatively, a + generator emitting the text to page. + :param color: controls if the pager supports ANSI colors or not. The + default is autodetection. + """ + color = resolve_color_default(color) + + if inspect.isgeneratorfunction(text_or_generator): + i = text_or_generator() + elif isinstance(text_or_generator, string_types): + i = [text_or_generator] + else: + i = iter(text_or_generator) + + # convert every element of i to a text type if necessary + text_generator = (el if isinstance(el, string_types) else text_type(el) + for el in i) + + from ._termui_impl import pager + return pager(itertools.chain(text_generator, "\n"), color) + + +def progressbar(iterable=None, length=None, label=None, show_eta=True, + show_percent=None, show_pos=False, + item_show_func=None, fill_char='#', empty_char='-', + bar_template='%(label)s [%(bar)s] %(info)s', + info_sep=' ', width=36, file=None, color=None): + """This function creates an iterable context manager that can be used + to iterate over something while showing a progress bar. It will + either iterate over the `iterable` or `length` items (that are counted + up). While iteration happens, this function will print a rendered + progress bar to the given `file` (defaults to stdout) and will attempt + to calculate remaining time and more. By default, this progress bar + will not be rendered if the file is not a terminal. + + The context manager creates the progress bar. When the context + manager is entered the progress bar is already displayed. With every + iteration over the progress bar, the iterable passed to the bar is + advanced and the bar is updated. When the context manager exits, + a newline is printed and the progress bar is finalized on screen. + + No printing must happen or the progress bar will be unintentionally + destroyed. + + Example usage:: + + with progressbar(items) as bar: + for item in bar: + do_something_with(item) + + Alternatively, if no iterable is specified, one can manually update the + progress bar through the `update()` method instead of directly + iterating over the progress bar. The update method accepts the number + of steps to increment the bar with:: + + with progressbar(length=chunks.total_bytes) as bar: + for chunk in chunks: + process_chunk(chunk) + bar.update(chunks.bytes) + + .. versionadded:: 2.0 + + .. versionadded:: 4.0 + Added the `color` parameter. Added a `update` method to the + progressbar object. + + :param iterable: an iterable to iterate over. If not provided the length + is required. + :param length: the number of items to iterate over. By default the + progressbar will attempt to ask the iterator about its + length, which might or might not work. If an iterable is + also provided this parameter can be used to override the + length. If an iterable is not provided the progress bar + will iterate over a range of that length. + :param label: the label to show next to the progress bar. + :param show_eta: enables or disables the estimated time display. This is + automatically disabled if the length cannot be + determined. + :param show_percent: enables or disables the percentage display. The + default is `True` if the iterable has a length or + `False` if not. + :param show_pos: enables or disables the absolute position display. The + default is `False`. + :param item_show_func: a function called with the current item which + can return a string to show the current item + next to the progress bar. Note that the current + item can be `None`! + :param fill_char: the character to use to show the filled part of the + progress bar. + :param empty_char: the character to use to show the non-filled part of + the progress bar. + :param bar_template: the format string to use as template for the bar. + The parameters in it are ``label`` for the label, + ``bar`` for the progress bar and ``info`` for the + info section. + :param info_sep: the separator between multiple info items (eta etc.) + :param width: the width of the progress bar in characters, 0 means full + terminal width + :param file: the file to write to. If this is not a terminal then + only the label is printed. + :param color: controls if the terminal supports ANSI colors or not. The + default is autodetection. This is only needed if ANSI + codes are included anywhere in the progress bar output + which is not the case by default. + """ + from ._termui_impl import ProgressBar + color = resolve_color_default(color) + return ProgressBar(iterable=iterable, length=length, show_eta=show_eta, + show_percent=show_percent, show_pos=show_pos, + item_show_func=item_show_func, fill_char=fill_char, + empty_char=empty_char, bar_template=bar_template, + info_sep=info_sep, file=file, label=label, + width=width, color=color) + + +def clear(): + """Clears the terminal screen. This will have the effect of clearing + the whole visible space of the terminal and moving the cursor to the + top left. This does not do anything if not connected to a terminal. + + .. versionadded:: 2.0 + """ + if not isatty(sys.stdout): + return + # If we're on Windows and we don't have colorama available, then we + # clear the screen by shelling out. Otherwise we can use an escape + # sequence. + if WIN: + os.system('cls') + else: + sys.stdout.write('\033[2J\033[1;1H') + + +def style(text, fg=None, bg=None, bold=None, dim=None, underline=None, + blink=None, reverse=None, reset=True): + """Styles a text with ANSI styles and returns the new string. By + default the styling is self contained which means that at the end + of the string a reset code is issued. This can be prevented by + passing ``reset=False``. + + Examples:: + + click.echo(click.style('Hello World!', fg='green')) + click.echo(click.style('ATTENTION!', blink=True)) + click.echo(click.style('Some things', reverse=True, fg='cyan')) + + Supported color names: + + * ``black`` (might be a gray) + * ``red`` + * ``green`` + * ``yellow`` (might be an orange) + * ``blue`` + * ``magenta`` + * ``cyan`` + * ``white`` (might be light gray) + * ``bright_black`` + * ``bright_red`` + * ``bright_green`` + * ``bright_yellow`` + * ``bright_blue`` + * ``bright_magenta`` + * ``bright_cyan`` + * ``bright_white`` + * ``reset`` (reset the color code only) + + .. versionadded:: 2.0 + + .. versionadded:: 7.0 + Added support for bright colors. + + :param text: the string to style with ansi codes. + :param fg: if provided this will become the foreground color. + :param bg: if provided this will become the background color. + :param bold: if provided this will enable or disable bold mode. + :param dim: if provided this will enable or disable dim mode. This is + badly supported. + :param underline: if provided this will enable or disable underline. + :param blink: if provided this will enable or disable blinking. + :param reverse: if provided this will enable or disable inverse + rendering (foreground becomes background and the + other way round). + :param reset: by default a reset-all code is added at the end of the + string which means that styles do not carry over. This + can be disabled to compose styles. + """ + bits = [] + if fg: + try: + bits.append('\033[%dm' % (_ansi_colors[fg])) + except KeyError: + raise TypeError('Unknown color %r' % fg) + if bg: + try: + bits.append('\033[%dm' % (_ansi_colors[bg] + 10)) + except KeyError: + raise TypeError('Unknown color %r' % bg) + if bold is not None: + bits.append('\033[%dm' % (1 if bold else 22)) + if dim is not None: + bits.append('\033[%dm' % (2 if dim else 22)) + if underline is not None: + bits.append('\033[%dm' % (4 if underline else 24)) + if blink is not None: + bits.append('\033[%dm' % (5 if blink else 25)) + if reverse is not None: + bits.append('\033[%dm' % (7 if reverse else 27)) + bits.append(text) + if reset: + bits.append(_ansi_reset_all) + return ''.join(bits) + + +def unstyle(text): + """Removes ANSI styling information from a string. Usually it's not + necessary to use this function as Click's echo function will + automatically remove styling if necessary. + + .. versionadded:: 2.0 + + :param text: the text to remove style information from. + """ + return strip_ansi(text) + + +def secho(message=None, file=None, nl=True, err=False, color=None, **styles): + """This function combines :func:`echo` and :func:`style` into one + call. As such the following two calls are the same:: + + click.secho('Hello World!', fg='green') + click.echo(click.style('Hello World!', fg='green')) + + All keyword arguments are forwarded to the underlying functions + depending on which one they go with. + + .. versionadded:: 2.0 + """ + if message is not None: + message = style(message, **styles) + return echo(message, file=file, nl=nl, err=err, color=color) + + +def edit(text=None, editor=None, env=None, require_save=True, + extension='.txt', filename=None): + r"""Edits the given text in the defined editor. If an editor is given + (should be the full path to the executable but the regular operating + system search path is used for finding the executable) it overrides + the detected editor. Optionally, some environment variables can be + used. If the editor is closed without changes, `None` is returned. In + case a file is edited directly the return value is always `None` and + `require_save` and `extension` are ignored. + + If the editor cannot be opened a :exc:`UsageError` is raised. + + Note for Windows: to simplify cross-platform usage, the newlines are + automatically converted from POSIX to Windows and vice versa. As such, + the message here will have ``\n`` as newline markers. + + :param text: the text to edit. + :param editor: optionally the editor to use. Defaults to automatic + detection. + :param env: environment variables to forward to the editor. + :param require_save: if this is true, then not saving in the editor + will make the return value become `None`. + :param extension: the extension to tell the editor about. This defaults + to `.txt` but changing this might change syntax + highlighting. + :param filename: if provided it will edit this file instead of the + provided text contents. It will not use a temporary + file as an indirection in that case. + """ + from ._termui_impl import Editor + editor = Editor(editor=editor, env=env, require_save=require_save, + extension=extension) + if filename is None: + return editor.edit(text) + editor.edit_file(filename) + + +def launch(url, wait=False, locate=False): + """This function launches the given URL (or filename) in the default + viewer application for this file type. If this is an executable, it + might launch the executable in a new session. The return value is + the exit code of the launched application. Usually, ``0`` indicates + success. + + Examples:: + + click.launch('https://click.palletsprojects.com/') + click.launch('/my/downloaded/file', locate=True) + + .. versionadded:: 2.0 + + :param url: URL or filename of the thing to launch. + :param wait: waits for the program to stop. + :param locate: if this is set to `True` then instead of launching the + application associated with the URL it will attempt to + launch a file manager with the file located. This + might have weird effects if the URL does not point to + the filesystem. + """ + from ._termui_impl import open_url + return open_url(url, wait=wait, locate=locate) + + +# If this is provided, getchar() calls into this instead. This is used +# for unittesting purposes. +_getchar = None + + +def getchar(echo=False): + """Fetches a single character from the terminal and returns it. This + will always return a unicode character and under certain rare + circumstances this might return more than one character. The + situations which more than one character is returned is when for + whatever reason multiple characters end up in the terminal buffer or + standard input was not actually a terminal. + + Note that this will always read from the terminal, even if something + is piped into the standard input. + + Note for Windows: in rare cases when typing non-ASCII characters, this + function might wait for a second character and then return both at once. + This is because certain Unicode characters look like special-key markers. + + .. versionadded:: 2.0 + + :param echo: if set to `True`, the character read will also show up on + the terminal. The default is to not show it. + """ + f = _getchar + if f is None: + from ._termui_impl import getchar as f + return f(echo) + + +def raw_terminal(): + from ._termui_impl import raw_terminal as f + return f() + + +def pause(info='Press any key to continue ...', err=False): + """This command stops execution and waits for the user to press any + key to continue. This is similar to the Windows batch "pause" + command. If the program is not run through a terminal, this command + will instead do nothing. + + .. versionadded:: 2.0 + + .. versionadded:: 4.0 + Added the `err` parameter. + + :param info: the info string to print before pausing. + :param err: if set to message goes to ``stderr`` instead of + ``stdout``, the same as with echo. + """ + if not isatty(sys.stdin) or not isatty(sys.stdout): + return + try: + if info: + echo(info, nl=False, err=err) + try: + getchar() + except (KeyboardInterrupt, EOFError): + pass + finally: + if info: + echo(err=err) diff --git a/libs/click/testing.py b/libs/click/testing.py new file mode 100644 index 00000000..1b2924e0 --- /dev/null +++ b/libs/click/testing.py @@ -0,0 +1,374 @@ +import os +import sys +import shutil +import tempfile +import contextlib +import shlex + +from ._compat import iteritems, PY2, string_types + + +# If someone wants to vendor click, we want to ensure the +# correct package is discovered. Ideally we could use a +# relative import here but unfortunately Python does not +# support that. +clickpkg = sys.modules[__name__.rsplit('.', 1)[0]] + + +if PY2: + from cStringIO import StringIO +else: + import io + from ._compat import _find_binary_reader + + +class EchoingStdin(object): + + def __init__(self, input, output): + self._input = input + self._output = output + + def __getattr__(self, x): + return getattr(self._input, x) + + def _echo(self, rv): + self._output.write(rv) + return rv + + def read(self, n=-1): + return self._echo(self._input.read(n)) + + def readline(self, n=-1): + return self._echo(self._input.readline(n)) + + def readlines(self): + return [self._echo(x) for x in self._input.readlines()] + + def __iter__(self): + return iter(self._echo(x) for x in self._input) + + def __repr__(self): + return repr(self._input) + + +def make_input_stream(input, charset): + # Is already an input stream. + if hasattr(input, 'read'): + if PY2: + return input + rv = _find_binary_reader(input) + if rv is not None: + return rv + raise TypeError('Could not find binary reader for input stream.') + + if input is None: + input = b'' + elif not isinstance(input, bytes): + input = input.encode(charset) + if PY2: + return StringIO(input) + return io.BytesIO(input) + + +class Result(object): + """Holds the captured result of an invoked CLI script.""" + + def __init__(self, runner, stdout_bytes, stderr_bytes, exit_code, + exception, exc_info=None): + #: The runner that created the result + self.runner = runner + #: The standard output as bytes. + self.stdout_bytes = stdout_bytes + #: The standard error as bytes, or False(y) if not available + self.stderr_bytes = stderr_bytes + #: The exit code as integer. + self.exit_code = exit_code + #: The exception that happened if one did. + self.exception = exception + #: The traceback + self.exc_info = exc_info + + @property + def output(self): + """The (standard) output as unicode string.""" + return self.stdout + + @property + def stdout(self): + """The standard output as unicode string.""" + return self.stdout_bytes.decode(self.runner.charset, 'replace') \ + .replace('\r\n', '\n') + + @property + def stderr(self): + """The standard error as unicode string.""" + if not self.stderr_bytes: + raise ValueError("stderr not separately captured") + return self.stderr_bytes.decode(self.runner.charset, 'replace') \ + .replace('\r\n', '\n') + + + def __repr__(self): + return '<%s %s>' % ( + type(self).__name__, + self.exception and repr(self.exception) or 'okay', + ) + + +class CliRunner(object): + """The CLI runner provides functionality to invoke a Click command line + script for unittesting purposes in a isolated environment. This only + works in single-threaded systems without any concurrency as it changes the + global interpreter state. + + :param charset: the character set for the input and output data. This is + UTF-8 by default and should not be changed currently as + the reporting to Click only works in Python 2 properly. + :param env: a dictionary with environment variables for overriding. + :param echo_stdin: if this is set to `True`, then reading from stdin writes + to stdout. This is useful for showing examples in + some circumstances. Note that regular prompts + will automatically echo the input. + :param mix_stderr: if this is set to `False`, then stdout and stderr are + preserved as independent streams. This is useful for + Unix-philosophy apps that have predictable stdout and + noisy stderr, such that each may be measured + independently + """ + + def __init__(self, charset=None, env=None, echo_stdin=False, + mix_stderr=True): + if charset is None: + charset = 'utf-8' + self.charset = charset + self.env = env or {} + self.echo_stdin = echo_stdin + self.mix_stderr = mix_stderr + + def get_default_prog_name(self, cli): + """Given a command object it will return the default program name + for it. The default is the `name` attribute or ``"root"`` if not + set. + """ + return cli.name or 'root' + + def make_env(self, overrides=None): + """Returns the environment overrides for invoking a script.""" + rv = dict(self.env) + if overrides: + rv.update(overrides) + return rv + + @contextlib.contextmanager + def isolation(self, input=None, env=None, color=False): + """A context manager that sets up the isolation for invoking of a + command line tool. This sets up stdin with the given input data + and `os.environ` with the overrides from the given dictionary. + This also rebinds some internals in Click to be mocked (like the + prompt functionality). + + This is automatically done in the :meth:`invoke` method. + + .. versionadded:: 4.0 + The ``color`` parameter was added. + + :param input: the input stream to put into sys.stdin. + :param env: the environment overrides as dictionary. + :param color: whether the output should contain color codes. The + application can still override this explicitly. + """ + input = make_input_stream(input, self.charset) + + old_stdin = sys.stdin + old_stdout = sys.stdout + old_stderr = sys.stderr + old_forced_width = clickpkg.formatting.FORCED_WIDTH + clickpkg.formatting.FORCED_WIDTH = 80 + + env = self.make_env(env) + + if PY2: + bytes_output = StringIO() + if self.echo_stdin: + input = EchoingStdin(input, bytes_output) + sys.stdout = bytes_output + if not self.mix_stderr: + bytes_error = StringIO() + sys.stderr = bytes_error + else: + bytes_output = io.BytesIO() + if self.echo_stdin: + input = EchoingStdin(input, bytes_output) + input = io.TextIOWrapper(input, encoding=self.charset) + sys.stdout = io.TextIOWrapper( + bytes_output, encoding=self.charset) + if not self.mix_stderr: + bytes_error = io.BytesIO() + sys.stderr = io.TextIOWrapper( + bytes_error, encoding=self.charset) + + if self.mix_stderr: + sys.stderr = sys.stdout + + sys.stdin = input + + def visible_input(prompt=None): + sys.stdout.write(prompt or '') + val = input.readline().rstrip('\r\n') + sys.stdout.write(val + '\n') + sys.stdout.flush() + return val + + def hidden_input(prompt=None): + sys.stdout.write((prompt or '') + '\n') + sys.stdout.flush() + return input.readline().rstrip('\r\n') + + def _getchar(echo): + char = sys.stdin.read(1) + if echo: + sys.stdout.write(char) + sys.stdout.flush() + return char + + default_color = color + + def should_strip_ansi(stream=None, color=None): + if color is None: + return not default_color + return not color + + old_visible_prompt_func = clickpkg.termui.visible_prompt_func + old_hidden_prompt_func = clickpkg.termui.hidden_prompt_func + old__getchar_func = clickpkg.termui._getchar + old_should_strip_ansi = clickpkg.utils.should_strip_ansi + clickpkg.termui.visible_prompt_func = visible_input + clickpkg.termui.hidden_prompt_func = hidden_input + clickpkg.termui._getchar = _getchar + clickpkg.utils.should_strip_ansi = should_strip_ansi + + old_env = {} + try: + for key, value in iteritems(env): + old_env[key] = os.environ.get(key) + if value is None: + try: + del os.environ[key] + except Exception: + pass + else: + os.environ[key] = value + yield (bytes_output, not self.mix_stderr and bytes_error) + finally: + for key, value in iteritems(old_env): + if value is None: + try: + del os.environ[key] + except Exception: + pass + else: + os.environ[key] = value + sys.stdout = old_stdout + sys.stderr = old_stderr + sys.stdin = old_stdin + clickpkg.termui.visible_prompt_func = old_visible_prompt_func + clickpkg.termui.hidden_prompt_func = old_hidden_prompt_func + clickpkg.termui._getchar = old__getchar_func + clickpkg.utils.should_strip_ansi = old_should_strip_ansi + clickpkg.formatting.FORCED_WIDTH = old_forced_width + + def invoke(self, cli, args=None, input=None, env=None, + catch_exceptions=True, color=False, mix_stderr=False, **extra): + """Invokes a command in an isolated environment. The arguments are + forwarded directly to the command line script, the `extra` keyword + arguments are passed to the :meth:`~clickpkg.Command.main` function of + the command. + + This returns a :class:`Result` object. + + .. versionadded:: 3.0 + The ``catch_exceptions`` parameter was added. + + .. versionchanged:: 3.0 + The result object now has an `exc_info` attribute with the + traceback if available. + + .. versionadded:: 4.0 + The ``color`` parameter was added. + + :param cli: the command to invoke + :param args: the arguments to invoke. It may be given as an iterable + or a string. When given as string it will be interpreted + as a Unix shell command. More details at + :func:`shlex.split`. + :param input: the input data for `sys.stdin`. + :param env: the environment overrides. + :param catch_exceptions: Whether to catch any other exceptions than + ``SystemExit``. + :param extra: the keyword arguments to pass to :meth:`main`. + :param color: whether the output should contain color codes. The + application can still override this explicitly. + """ + exc_info = None + with self.isolation(input=input, env=env, color=color) as outstreams: + exception = None + exit_code = 0 + + if isinstance(args, string_types): + args = shlex.split(args) + + try: + prog_name = extra.pop("prog_name") + except KeyError: + prog_name = self.get_default_prog_name(cli) + + try: + cli.main(args=args or (), prog_name=prog_name, **extra) + except SystemExit as e: + exc_info = sys.exc_info() + exit_code = e.code + if exit_code is None: + exit_code = 0 + + if exit_code != 0: + exception = e + + if not isinstance(exit_code, int): + sys.stdout.write(str(exit_code)) + sys.stdout.write('\n') + exit_code = 1 + + except Exception as e: + if not catch_exceptions: + raise + exception = e + exit_code = 1 + exc_info = sys.exc_info() + finally: + sys.stdout.flush() + stdout = outstreams[0].getvalue() + stderr = outstreams[1] and outstreams[1].getvalue() + + return Result(runner=self, + stdout_bytes=stdout, + stderr_bytes=stderr, + exit_code=exit_code, + exception=exception, + exc_info=exc_info) + + @contextlib.contextmanager + def isolated_filesystem(self): + """A context manager that creates a temporary folder and changes + the current working directory to it for isolated filesystem tests. + """ + cwd = os.getcwd() + t = tempfile.mkdtemp() + os.chdir(t) + try: + yield t + finally: + os.chdir(cwd) + try: + shutil.rmtree(t) + except (OSError, IOError): + pass diff --git a/libs/click/types.py b/libs/click/types.py new file mode 100644 index 00000000..1f88032f --- /dev/null +++ b/libs/click/types.py @@ -0,0 +1,668 @@ +import os +import stat +from datetime import datetime + +from ._compat import open_stream, text_type, filename_to_ui, \ + get_filesystem_encoding, get_streerror, _get_argv_encoding, PY2 +from .exceptions import BadParameter +from .utils import safecall, LazyFile + + +class ParamType(object): + """Helper for converting values through types. The following is + necessary for a valid type: + + * it needs a name + * it needs to pass through None unchanged + * it needs to convert from a string + * it needs to convert its result type through unchanged + (eg: needs to be idempotent) + * it needs to be able to deal with param and context being `None`. + This can be the case when the object is used with prompt + inputs. + """ + is_composite = False + + #: the descriptive name of this type + name = None + + #: if a list of this type is expected and the value is pulled from a + #: string environment variable, this is what splits it up. `None` + #: means any whitespace. For all parameters the general rule is that + #: whitespace splits them up. The exception are paths and files which + #: are split by ``os.path.pathsep`` by default (":" on Unix and ";" on + #: Windows). + envvar_list_splitter = None + + def __call__(self, value, param=None, ctx=None): + if value is not None: + return self.convert(value, param, ctx) + + def get_metavar(self, param): + """Returns the metavar default for this param if it provides one.""" + + def get_missing_message(self, param): + """Optionally might return extra information about a missing + parameter. + + .. versionadded:: 2.0 + """ + + def convert(self, value, param, ctx): + """Converts the value. This is not invoked for values that are + `None` (the missing value). + """ + return value + + def split_envvar_value(self, rv): + """Given a value from an environment variable this splits it up + into small chunks depending on the defined envvar list splitter. + + If the splitter is set to `None`, which means that whitespace splits, + then leading and trailing whitespace is ignored. Otherwise, leading + and trailing splitters usually lead to empty items being included. + """ + return (rv or '').split(self.envvar_list_splitter) + + def fail(self, message, param=None, ctx=None): + """Helper method to fail with an invalid value message.""" + raise BadParameter(message, ctx=ctx, param=param) + + +class CompositeParamType(ParamType): + is_composite = True + + @property + def arity(self): + raise NotImplementedError() + + +class FuncParamType(ParamType): + + def __init__(self, func): + self.name = func.__name__ + self.func = func + + def convert(self, value, param, ctx): + try: + return self.func(value) + except ValueError: + try: + value = text_type(value) + except UnicodeError: + value = str(value).decode('utf-8', 'replace') + self.fail(value, param, ctx) + + +class UnprocessedParamType(ParamType): + name = 'text' + + def convert(self, value, param, ctx): + return value + + def __repr__(self): + return 'UNPROCESSED' + + +class StringParamType(ParamType): + name = 'text' + + def convert(self, value, param, ctx): + if isinstance(value, bytes): + enc = _get_argv_encoding() + try: + value = value.decode(enc) + except UnicodeError: + fs_enc = get_filesystem_encoding() + if fs_enc != enc: + try: + value = value.decode(fs_enc) + except UnicodeError: + value = value.decode('utf-8', 'replace') + return value + return value + + def __repr__(self): + return 'STRING' + + +class Choice(ParamType): + """The choice type allows a value to be checked against a fixed set + of supported values. All of these values have to be strings. + + You should only pass a list or tuple of choices. Other iterables + (like generators) may lead to surprising results. + + See :ref:`choice-opts` for an example. + + :param case_sensitive: Set to false to make choices case + insensitive. Defaults to true. + """ + + name = 'choice' + + def __init__(self, choices, case_sensitive=True): + self.choices = choices + self.case_sensitive = case_sensitive + + def get_metavar(self, param): + return '[%s]' % '|'.join(self.choices) + + def get_missing_message(self, param): + return 'Choose from:\n\t%s.' % ',\n\t'.join(self.choices) + + def convert(self, value, param, ctx): + # Exact match + if value in self.choices: + return value + + # Match through normalization and case sensitivity + # first do token_normalize_func, then lowercase + # preserve original `value` to produce an accurate message in + # `self.fail` + normed_value = value + normed_choices = self.choices + + if ctx is not None and \ + ctx.token_normalize_func is not None: + normed_value = ctx.token_normalize_func(value) + normed_choices = [ctx.token_normalize_func(choice) for choice in + self.choices] + + if not self.case_sensitive: + normed_value = normed_value.lower() + normed_choices = [choice.lower() for choice in normed_choices] + + if normed_value in normed_choices: + return normed_value + + self.fail('invalid choice: %s. (choose from %s)' % + (value, ', '.join(self.choices)), param, ctx) + + def __repr__(self): + return 'Choice(%r)' % list(self.choices) + + +class DateTime(ParamType): + """The DateTime type converts date strings into `datetime` objects. + + The format strings which are checked are configurable, but default to some + common (non-timezone aware) ISO 8601 formats. + + When specifying *DateTime* formats, you should only pass a list or a tuple. + Other iterables, like generators, may lead to surprising results. + + The format strings are processed using ``datetime.strptime``, and this + consequently defines the format strings which are allowed. + + Parsing is tried using each format, in order, and the first format which + parses successfully is used. + + :param formats: A list or tuple of date format strings, in the order in + which they should be tried. Defaults to + ``'%Y-%m-%d'``, ``'%Y-%m-%dT%H:%M:%S'``, + ``'%Y-%m-%d %H:%M:%S'``. + """ + name = 'datetime' + + def __init__(self, formats=None): + self.formats = formats or [ + '%Y-%m-%d', + '%Y-%m-%dT%H:%M:%S', + '%Y-%m-%d %H:%M:%S' + ] + + def get_metavar(self, param): + return '[{}]'.format('|'.join(self.formats)) + + def _try_to_convert_date(self, value, format): + try: + return datetime.strptime(value, format) + except ValueError: + return None + + def convert(self, value, param, ctx): + # Exact match + for format in self.formats: + dtime = self._try_to_convert_date(value, format) + if dtime: + return dtime + + self.fail( + 'invalid datetime format: {}. (choose from {})'.format( + value, ', '.join(self.formats))) + + def __repr__(self): + return 'DateTime' + + +class IntParamType(ParamType): + name = 'integer' + + def convert(self, value, param, ctx): + try: + return int(value) + except (ValueError, UnicodeError): + self.fail('%s is not a valid integer' % value, param, ctx) + + def __repr__(self): + return 'INT' + + +class IntRange(IntParamType): + """A parameter that works similar to :data:`click.INT` but restricts + the value to fit into a range. The default behavior is to fail if the + value falls outside the range, but it can also be silently clamped + between the two edges. + + See :ref:`ranges` for an example. + """ + name = 'integer range' + + def __init__(self, min=None, max=None, clamp=False): + self.min = min + self.max = max + self.clamp = clamp + + def convert(self, value, param, ctx): + rv = IntParamType.convert(self, value, param, ctx) + if self.clamp: + if self.min is not None and rv < self.min: + return self.min + if self.max is not None and rv > self.max: + return self.max + if self.min is not None and rv < self.min or \ + self.max is not None and rv > self.max: + if self.min is None: + self.fail('%s is bigger than the maximum valid value ' + '%s.' % (rv, self.max), param, ctx) + elif self.max is None: + self.fail('%s is smaller than the minimum valid value ' + '%s.' % (rv, self.min), param, ctx) + else: + self.fail('%s is not in the valid range of %s to %s.' + % (rv, self.min, self.max), param, ctx) + return rv + + def __repr__(self): + return 'IntRange(%r, %r)' % (self.min, self.max) + + +class FloatParamType(ParamType): + name = 'float' + + def convert(self, value, param, ctx): + try: + return float(value) + except (UnicodeError, ValueError): + self.fail('%s is not a valid floating point value' % + value, param, ctx) + + def __repr__(self): + return 'FLOAT' + + +class FloatRange(FloatParamType): + """A parameter that works similar to :data:`click.FLOAT` but restricts + the value to fit into a range. The default behavior is to fail if the + value falls outside the range, but it can also be silently clamped + between the two edges. + + See :ref:`ranges` for an example. + """ + name = 'float range' + + def __init__(self, min=None, max=None, clamp=False): + self.min = min + self.max = max + self.clamp = clamp + + def convert(self, value, param, ctx): + rv = FloatParamType.convert(self, value, param, ctx) + if self.clamp: + if self.min is not None and rv < self.min: + return self.min + if self.max is not None and rv > self.max: + return self.max + if self.min is not None and rv < self.min or \ + self.max is not None and rv > self.max: + if self.min is None: + self.fail('%s is bigger than the maximum valid value ' + '%s.' % (rv, self.max), param, ctx) + elif self.max is None: + self.fail('%s is smaller than the minimum valid value ' + '%s.' % (rv, self.min), param, ctx) + else: + self.fail('%s is not in the valid range of %s to %s.' + % (rv, self.min, self.max), param, ctx) + return rv + + def __repr__(self): + return 'FloatRange(%r, %r)' % (self.min, self.max) + + +class BoolParamType(ParamType): + name = 'boolean' + + def convert(self, value, param, ctx): + if isinstance(value, bool): + return bool(value) + value = value.lower() + if value in ('true', 't', '1', 'yes', 'y'): + return True + elif value in ('false', 'f', '0', 'no', 'n'): + return False + self.fail('%s is not a valid boolean' % value, param, ctx) + + def __repr__(self): + return 'BOOL' + + +class UUIDParameterType(ParamType): + name = 'uuid' + + def convert(self, value, param, ctx): + import uuid + try: + if PY2 and isinstance(value, text_type): + value = value.encode('ascii') + return uuid.UUID(value) + except (UnicodeError, ValueError): + self.fail('%s is not a valid UUID value' % value, param, ctx) + + def __repr__(self): + return 'UUID' + + +class File(ParamType): + """Declares a parameter to be a file for reading or writing. The file + is automatically closed once the context tears down (after the command + finished working). + + Files can be opened for reading or writing. The special value ``-`` + indicates stdin or stdout depending on the mode. + + By default, the file is opened for reading text data, but it can also be + opened in binary mode or for writing. The encoding parameter can be used + to force a specific encoding. + + The `lazy` flag controls if the file should be opened immediately or upon + first IO. The default is to be non-lazy for standard input and output + streams as well as files opened for reading, `lazy` otherwise. When opening a + file lazily for reading, it is still opened temporarily for validation, but + will not be held open until first IO. lazy is mainly useful when opening + for writing to avoid creating the file until it is needed. + + Starting with Click 2.0, files can also be opened atomically in which + case all writes go into a separate file in the same folder and upon + completion the file will be moved over to the original location. This + is useful if a file regularly read by other users is modified. + + See :ref:`file-args` for more information. + """ + name = 'filename' + envvar_list_splitter = os.path.pathsep + + def __init__(self, mode='r', encoding=None, errors='strict', lazy=None, + atomic=False): + self.mode = mode + self.encoding = encoding + self.errors = errors + self.lazy = lazy + self.atomic = atomic + + def resolve_lazy_flag(self, value): + if self.lazy is not None: + return self.lazy + if value == '-': + return False + elif 'w' in self.mode: + return True + return False + + def convert(self, value, param, ctx): + try: + if hasattr(value, 'read') or hasattr(value, 'write'): + return value + + lazy = self.resolve_lazy_flag(value) + + if lazy: + f = LazyFile(value, self.mode, self.encoding, self.errors, + atomic=self.atomic) + if ctx is not None: + ctx.call_on_close(f.close_intelligently) + return f + + f, should_close = open_stream(value, self.mode, + self.encoding, self.errors, + atomic=self.atomic) + # If a context is provided, we automatically close the file + # at the end of the context execution (or flush out). If a + # context does not exist, it's the caller's responsibility to + # properly close the file. This for instance happens when the + # type is used with prompts. + if ctx is not None: + if should_close: + ctx.call_on_close(safecall(f.close)) + else: + ctx.call_on_close(safecall(f.flush)) + return f + except (IOError, OSError) as e: + self.fail('Could not open file: %s: %s' % ( + filename_to_ui(value), + get_streerror(e), + ), param, ctx) + + +class Path(ParamType): + """The path type is similar to the :class:`File` type but it performs + different checks. First of all, instead of returning an open file + handle it returns just the filename. Secondly, it can perform various + basic checks about what the file or directory should be. + + .. versionchanged:: 6.0 + `allow_dash` was added. + + :param exists: if set to true, the file or directory needs to exist for + this value to be valid. If this is not required and a + file does indeed not exist, then all further checks are + silently skipped. + :param file_okay: controls if a file is a possible value. + :param dir_okay: controls if a directory is a possible value. + :param writable: if true, a writable check is performed. + :param readable: if true, a readable check is performed. + :param resolve_path: if this is true, then the path is fully resolved + before the value is passed onwards. This means + that it's absolute and symlinks are resolved. It + will not expand a tilde-prefix, as this is + supposed to be done by the shell only. + :param allow_dash: If this is set to `True`, a single dash to indicate + standard streams is permitted. + :param path_type: optionally a string type that should be used to + represent the path. The default is `None` which + means the return value will be either bytes or + unicode depending on what makes most sense given the + input data Click deals with. + """ + envvar_list_splitter = os.path.pathsep + + def __init__(self, exists=False, file_okay=True, dir_okay=True, + writable=False, readable=True, resolve_path=False, + allow_dash=False, path_type=None): + self.exists = exists + self.file_okay = file_okay + self.dir_okay = dir_okay + self.writable = writable + self.readable = readable + self.resolve_path = resolve_path + self.allow_dash = allow_dash + self.type = path_type + + if self.file_okay and not self.dir_okay: + self.name = 'file' + self.path_type = 'File' + elif self.dir_okay and not self.file_okay: + self.name = 'directory' + self.path_type = 'Directory' + else: + self.name = 'path' + self.path_type = 'Path' + + def coerce_path_result(self, rv): + if self.type is not None and not isinstance(rv, self.type): + if self.type is text_type: + rv = rv.decode(get_filesystem_encoding()) + else: + rv = rv.encode(get_filesystem_encoding()) + return rv + + def convert(self, value, param, ctx): + rv = value + + is_dash = self.file_okay and self.allow_dash and rv in (b'-', '-') + + if not is_dash: + if self.resolve_path: + rv = os.path.realpath(rv) + + try: + st = os.stat(rv) + except OSError: + if not self.exists: + return self.coerce_path_result(rv) + self.fail('%s "%s" does not exist.' % ( + self.path_type, + filename_to_ui(value) + ), param, ctx) + + if not self.file_okay and stat.S_ISREG(st.st_mode): + self.fail('%s "%s" is a file.' % ( + self.path_type, + filename_to_ui(value) + ), param, ctx) + if not self.dir_okay and stat.S_ISDIR(st.st_mode): + self.fail('%s "%s" is a directory.' % ( + self.path_type, + filename_to_ui(value) + ), param, ctx) + if self.writable and not os.access(value, os.W_OK): + self.fail('%s "%s" is not writable.' % ( + self.path_type, + filename_to_ui(value) + ), param, ctx) + if self.readable and not os.access(value, os.R_OK): + self.fail('%s "%s" is not readable.' % ( + self.path_type, + filename_to_ui(value) + ), param, ctx) + + return self.coerce_path_result(rv) + + +class Tuple(CompositeParamType): + """The default behavior of Click is to apply a type on a value directly. + This works well in most cases, except for when `nargs` is set to a fixed + count and different types should be used for different items. In this + case the :class:`Tuple` type can be used. This type can only be used + if `nargs` is set to a fixed number. + + For more information see :ref:`tuple-type`. + + This can be selected by using a Python tuple literal as a type. + + :param types: a list of types that should be used for the tuple items. + """ + + def __init__(self, types): + self.types = [convert_type(ty) for ty in types] + + @property + def name(self): + return "<" + " ".join(ty.name for ty in self.types) + ">" + + @property + def arity(self): + return len(self.types) + + def convert(self, value, param, ctx): + if len(value) != len(self.types): + raise TypeError('It would appear that nargs is set to conflict ' + 'with the composite type arity.') + return tuple(ty(x, param, ctx) for ty, x in zip(self.types, value)) + + +def convert_type(ty, default=None): + """Converts a callable or python ty into the most appropriate param + ty. + """ + guessed_type = False + if ty is None and default is not None: + if isinstance(default, tuple): + ty = tuple(map(type, default)) + else: + ty = type(default) + guessed_type = True + + if isinstance(ty, tuple): + return Tuple(ty) + if isinstance(ty, ParamType): + return ty + if ty is text_type or ty is str or ty is None: + return STRING + if ty is int: + return INT + # Booleans are only okay if not guessed. This is done because for + # flags the default value is actually a bit of a lie in that it + # indicates which of the flags is the one we want. See get_default() + # for more information. + if ty is bool and not guessed_type: + return BOOL + if ty is float: + return FLOAT + if guessed_type: + return STRING + + # Catch a common mistake + if __debug__: + try: + if issubclass(ty, ParamType): + raise AssertionError('Attempted to use an uninstantiated ' + 'parameter type (%s).' % ty) + except TypeError: + pass + return FuncParamType(ty) + + +#: A dummy parameter type that just does nothing. From a user's +#: perspective this appears to just be the same as `STRING` but internally +#: no string conversion takes place. This is necessary to achieve the +#: same bytes/unicode behavior on Python 2/3 in situations where you want +#: to not convert argument types. This is usually useful when working +#: with file paths as they can appear in bytes and unicode. +#: +#: For path related uses the :class:`Path` type is a better choice but +#: there are situations where an unprocessed type is useful which is why +#: it is is provided. +#: +#: .. versionadded:: 4.0 +UNPROCESSED = UnprocessedParamType() + +#: A unicode string parameter type which is the implicit default. This +#: can also be selected by using ``str`` as type. +STRING = StringParamType() + +#: An integer parameter. This can also be selected by using ``int`` as +#: type. +INT = IntParamType() + +#: A floating point value parameter. This can also be selected by using +#: ``float`` as type. +FLOAT = FloatParamType() + +#: A boolean parameter. This is the default for boolean flags. This can +#: also be selected by using ``bool`` as a type. +BOOL = BoolParamType() + +#: A UUID parameter. +UUID = UUIDParameterType() diff --git a/libs/click/utils.py b/libs/click/utils.py new file mode 100644 index 00000000..fc84369f --- /dev/null +++ b/libs/click/utils.py @@ -0,0 +1,440 @@ +import os +import sys + +from .globals import resolve_color_default + +from ._compat import text_type, open_stream, get_filesystem_encoding, \ + get_streerror, string_types, PY2, binary_streams, text_streams, \ + filename_to_ui, auto_wrap_for_ansi, strip_ansi, should_strip_ansi, \ + _default_text_stdout, _default_text_stderr, is_bytes, WIN + +if not PY2: + from ._compat import _find_binary_writer +elif WIN: + from ._winconsole import _get_windows_argv, \ + _hash_py_argv, _initial_argv_hash + + +echo_native_types = string_types + (bytes, bytearray) + + +def _posixify(name): + return '-'.join(name.split()).lower() + + +def safecall(func): + """Wraps a function so that it swallows exceptions.""" + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception: + pass + return wrapper + + +def make_str(value): + """Converts a value into a valid string.""" + if isinstance(value, bytes): + try: + return value.decode(get_filesystem_encoding()) + except UnicodeError: + return value.decode('utf-8', 'replace') + return text_type(value) + + +def make_default_short_help(help, max_length=45): + """Return a condensed version of help string.""" + words = help.split() + total_length = 0 + result = [] + done = False + + for word in words: + if word[-1:] == '.': + done = True + new_length = result and 1 + len(word) or len(word) + if total_length + new_length > max_length: + result.append('...') + done = True + else: + if result: + result.append(' ') + result.append(word) + if done: + break + total_length += new_length + + return ''.join(result) + + +class LazyFile(object): + """A lazy file works like a regular file but it does not fully open + the file but it does perform some basic checks early to see if the + filename parameter does make sense. This is useful for safely opening + files for writing. + """ + + def __init__(self, filename, mode='r', encoding=None, errors='strict', + atomic=False): + self.name = filename + self.mode = mode + self.encoding = encoding + self.errors = errors + self.atomic = atomic + + if filename == '-': + self._f, self.should_close = open_stream(filename, mode, + encoding, errors) + else: + if 'r' in mode: + # Open and close the file in case we're opening it for + # reading so that we can catch at least some errors in + # some cases early. + open(filename, mode).close() + self._f = None + self.should_close = True + + def __getattr__(self, name): + return getattr(self.open(), name) + + def __repr__(self): + if self._f is not None: + return repr(self._f) + return '' % (self.name, self.mode) + + def open(self): + """Opens the file if it's not yet open. This call might fail with + a :exc:`FileError`. Not handling this error will produce an error + that Click shows. + """ + if self._f is not None: + return self._f + try: + rv, self.should_close = open_stream(self.name, self.mode, + self.encoding, + self.errors, + atomic=self.atomic) + except (IOError, OSError) as e: + from .exceptions import FileError + raise FileError(self.name, hint=get_streerror(e)) + self._f = rv + return rv + + def close(self): + """Closes the underlying file, no matter what.""" + if self._f is not None: + self._f.close() + + def close_intelligently(self): + """This function only closes the file if it was opened by the lazy + file wrapper. For instance this will never close stdin. + """ + if self.should_close: + self.close() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, tb): + self.close_intelligently() + + def __iter__(self): + self.open() + return iter(self._f) + + +class KeepOpenFile(object): + + def __init__(self, file): + self._file = file + + def __getattr__(self, name): + return getattr(self._file, name) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, tb): + pass + + def __repr__(self): + return repr(self._file) + + def __iter__(self): + return iter(self._file) + + +def echo(message=None, file=None, nl=True, err=False, color=None): + """Prints a message plus a newline to the given file or stdout. On + first sight, this looks like the print function, but it has improved + support for handling Unicode and binary data that does not fail no + matter how badly configured the system is. + + Primarily it means that you can print binary data as well as Unicode + data on both 2.x and 3.x to the given file in the most appropriate way + possible. This is a very carefree function in that it will try its + best to not fail. As of Click 6.0 this includes support for unicode + output on the Windows console. + + In addition to that, if `colorama`_ is installed, the echo function will + also support clever handling of ANSI codes. Essentially it will then + do the following: + + - add transparent handling of ANSI color codes on Windows. + - hide ANSI codes automatically if the destination file is not a + terminal. + + .. _colorama: https://pypi.org/project/colorama/ + + .. versionchanged:: 6.0 + As of Click 6.0 the echo function will properly support unicode + output on the windows console. Not that click does not modify + the interpreter in any way which means that `sys.stdout` or the + print statement or function will still not provide unicode support. + + .. versionchanged:: 2.0 + Starting with version 2.0 of Click, the echo function will work + with colorama if it's installed. + + .. versionadded:: 3.0 + The `err` parameter was added. + + .. versionchanged:: 4.0 + Added the `color` flag. + + :param message: the message to print + :param file: the file to write to (defaults to ``stdout``) + :param err: if set to true the file defaults to ``stderr`` instead of + ``stdout``. This is faster and easier than calling + :func:`get_text_stderr` yourself. + :param nl: if set to `True` (the default) a newline is printed afterwards. + :param color: controls if the terminal supports ANSI colors or not. The + default is autodetection. + """ + if file is None: + if err: + file = _default_text_stderr() + else: + file = _default_text_stdout() + + # Convert non bytes/text into the native string type. + if message is not None and not isinstance(message, echo_native_types): + message = text_type(message) + + if nl: + message = message or u'' + if isinstance(message, text_type): + message += u'\n' + else: + message += b'\n' + + # If there is a message, and we're in Python 3, and the value looks + # like bytes, we manually need to find the binary stream and write the + # message in there. This is done separately so that most stream + # types will work as you would expect. Eg: you can write to StringIO + # for other cases. + if message and not PY2 and is_bytes(message): + binary_file = _find_binary_writer(file) + if binary_file is not None: + file.flush() + binary_file.write(message) + binary_file.flush() + return + + # ANSI-style support. If there is no message or we are dealing with + # bytes nothing is happening. If we are connected to a file we want + # to strip colors. If we are on windows we either wrap the stream + # to strip the color or we use the colorama support to translate the + # ansi codes to API calls. + if message and not is_bytes(message): + color = resolve_color_default(color) + if should_strip_ansi(file, color): + message = strip_ansi(message) + elif WIN: + if auto_wrap_for_ansi is not None: + file = auto_wrap_for_ansi(file) + elif not color: + message = strip_ansi(message) + + if message: + file.write(message) + file.flush() + + +def get_binary_stream(name): + """Returns a system stream for byte processing. This essentially + returns the stream from the sys module with the given name but it + solves some compatibility issues between different Python versions. + Primarily this function is necessary for getting binary streams on + Python 3. + + :param name: the name of the stream to open. Valid names are ``'stdin'``, + ``'stdout'`` and ``'stderr'`` + """ + opener = binary_streams.get(name) + if opener is None: + raise TypeError('Unknown standard stream %r' % name) + return opener() + + +def get_text_stream(name, encoding=None, errors='strict'): + """Returns a system stream for text processing. This usually returns + a wrapped stream around a binary stream returned from + :func:`get_binary_stream` but it also can take shortcuts on Python 3 + for already correctly configured streams. + + :param name: the name of the stream to open. Valid names are ``'stdin'``, + ``'stdout'`` and ``'stderr'`` + :param encoding: overrides the detected default encoding. + :param errors: overrides the default error mode. + """ + opener = text_streams.get(name) + if opener is None: + raise TypeError('Unknown standard stream %r' % name) + return opener(encoding, errors) + + +def open_file(filename, mode='r', encoding=None, errors='strict', + lazy=False, atomic=False): + """This is similar to how the :class:`File` works but for manual + usage. Files are opened non lazy by default. This can open regular + files as well as stdin/stdout if ``'-'`` is passed. + + If stdin/stdout is returned the stream is wrapped so that the context + manager will not close the stream accidentally. This makes it possible + to always use the function like this without having to worry to + accidentally close a standard stream:: + + with open_file(filename) as f: + ... + + .. versionadded:: 3.0 + + :param filename: the name of the file to open (or ``'-'`` for stdin/stdout). + :param mode: the mode in which to open the file. + :param encoding: the encoding to use. + :param errors: the error handling for this file. + :param lazy: can be flipped to true to open the file lazily. + :param atomic: in atomic mode writes go into a temporary file and it's + moved on close. + """ + if lazy: + return LazyFile(filename, mode, encoding, errors, atomic=atomic) + f, should_close = open_stream(filename, mode, encoding, errors, + atomic=atomic) + if not should_close: + f = KeepOpenFile(f) + return f + + +def get_os_args(): + """This returns the argument part of sys.argv in the most appropriate + form for processing. What this means is that this return value is in + a format that works for Click to process but does not necessarily + correspond well to what's actually standard for the interpreter. + + On most environments the return value is ``sys.argv[:1]`` unchanged. + However if you are on Windows and running Python 2 the return value + will actually be a list of unicode strings instead because the + default behavior on that platform otherwise will not be able to + carry all possible values that sys.argv can have. + + .. versionadded:: 6.0 + """ + # We can only extract the unicode argv if sys.argv has not been + # changed since the startup of the application. + if PY2 and WIN and _initial_argv_hash == _hash_py_argv(): + return _get_windows_argv() + return sys.argv[1:] + + +def format_filename(filename, shorten=False): + """Formats a filename for user display. The main purpose of this + function is to ensure that the filename can be displayed at all. This + will decode the filename to unicode if necessary in a way that it will + not fail. Optionally, it can shorten the filename to not include the + full path to the filename. + + :param filename: formats a filename for UI display. This will also convert + the filename into unicode without failing. + :param shorten: this optionally shortens the filename to strip of the + path that leads up to it. + """ + if shorten: + filename = os.path.basename(filename) + return filename_to_ui(filename) + + +def get_app_dir(app_name, roaming=True, force_posix=False): + r"""Returns the config folder for the application. The default behavior + is to return whatever is most appropriate for the operating system. + + To give you an idea, for an app called ``"Foo Bar"``, something like + the following folders could be returned: + + Mac OS X: + ``~/Library/Application Support/Foo Bar`` + Mac OS X (POSIX): + ``~/.foo-bar`` + Unix: + ``~/.config/foo-bar`` + Unix (POSIX): + ``~/.foo-bar`` + Win XP (roaming): + ``C:\Documents and Settings\\Local Settings\Application Data\Foo Bar`` + Win XP (not roaming): + ``C:\Documents and Settings\\Application Data\Foo Bar`` + Win 7 (roaming): + ``C:\Users\\AppData\Roaming\Foo Bar`` + Win 7 (not roaming): + ``C:\Users\\AppData\Local\Foo Bar`` + + .. versionadded:: 2.0 + + :param app_name: the application name. This should be properly capitalized + and can contain whitespace. + :param roaming: controls if the folder should be roaming or not on Windows. + Has no affect otherwise. + :param force_posix: if this is set to `True` then on any POSIX system the + folder will be stored in the home folder with a leading + dot instead of the XDG config home or darwin's + application support folder. + """ + if WIN: + key = roaming and 'APPDATA' or 'LOCALAPPDATA' + folder = os.environ.get(key) + if folder is None: + folder = os.path.expanduser('~') + return os.path.join(folder, app_name) + if force_posix: + return os.path.join(os.path.expanduser('~/.' + _posixify(app_name))) + if sys.platform == 'darwin': + return os.path.join(os.path.expanduser( + '~/Library/Application Support'), app_name) + return os.path.join( + os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config')), + _posixify(app_name)) + + +class PacifyFlushWrapper(object): + """This wrapper is used to catch and suppress BrokenPipeErrors resulting + from ``.flush()`` being called on broken pipe during the shutdown/final-GC + of the Python interpreter. Notably ``.flush()`` is always called on + ``sys.stdout`` and ``sys.stderr``. So as to have minimal impact on any + other cleanup code, and the case where the underlying file is not a broken + pipe, all calls and attributes are proxied. + """ + + def __init__(self, wrapped): + self.wrapped = wrapped + + def flush(self): + try: + self.wrapped.flush() + except IOError as e: + import errno + if e.errno != errno.EPIPE: + raise + + def __getattr__(self, attr): + return getattr(self.wrapped, attr) diff --git a/libs/decorator.py b/libs/decorator.py new file mode 100644 index 00000000..44303eed --- /dev/null +++ b/libs/decorator.py @@ -0,0 +1,432 @@ +# ######################### LICENSE ############################ # + +# Copyright (c) 2005-2018, Michele Simionato +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: + +# Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# Redistributions in bytecode form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +# DAMAGE. + +""" +Decorator module, see http://pypi.python.org/pypi/decorator +for the documentation. +""" +from __future__ import print_function + +import re +import sys +import inspect +import operator +import itertools +import collections + +__version__ = '4.3.0' + +if sys.version >= '3': + from inspect import getfullargspec + + def get_init(cls): + return cls.__init__ +else: + FullArgSpec = collections.namedtuple( + 'FullArgSpec', 'args varargs varkw defaults ' + 'kwonlyargs kwonlydefaults annotations') + + def getfullargspec(f): + "A quick and dirty replacement for getfullargspec for Python 2.X" + return FullArgSpec._make(inspect.getargspec(f) + ([], None, {})) + + def get_init(cls): + return cls.__init__.__func__ + +try: + iscoroutinefunction = inspect.iscoroutinefunction +except AttributeError: + # let's assume there are no coroutine functions in old Python + def iscoroutinefunction(f): + return False + + +DEF = re.compile(r'\s*def\s*([_\w][_\w\d]*)\s*\(') + + +# basic functionality +class FunctionMaker(object): + """ + An object with the ability to create functions with a given signature. + It has attributes name, doc, module, signature, defaults, dict and + methods update and make. + """ + + # Atomic get-and-increment provided by the GIL + _compile_count = itertools.count() + + # make pylint happy + args = varargs = varkw = defaults = kwonlyargs = kwonlydefaults = () + + def __init__(self, func=None, name=None, signature=None, + defaults=None, doc=None, module=None, funcdict=None): + self.shortsignature = signature + if func: + # func can be a class or a callable, but not an instance method + self.name = func.__name__ + if self.name == '': # small hack for lambda functions + self.name = '_lambda_' + self.doc = func.__doc__ + self.module = func.__module__ + if inspect.isfunction(func): + argspec = getfullargspec(func) + self.annotations = getattr(func, '__annotations__', {}) + for a in ('args', 'varargs', 'varkw', 'defaults', 'kwonlyargs', + 'kwonlydefaults'): + setattr(self, a, getattr(argspec, a)) + for i, arg in enumerate(self.args): + setattr(self, 'arg%d' % i, arg) + allargs = list(self.args) + allshortargs = list(self.args) + if self.varargs: + allargs.append('*' + self.varargs) + allshortargs.append('*' + self.varargs) + elif self.kwonlyargs: + allargs.append('*') # single star syntax + for a in self.kwonlyargs: + allargs.append('%s=None' % a) + allshortargs.append('%s=%s' % (a, a)) + if self.varkw: + allargs.append('**' + self.varkw) + allshortargs.append('**' + self.varkw) + self.signature = ', '.join(allargs) + self.shortsignature = ', '.join(allshortargs) + self.dict = func.__dict__.copy() + # func=None happens when decorating a caller + if name: + self.name = name + if signature is not None: + self.signature = signature + if defaults: + self.defaults = defaults + if doc: + self.doc = doc + if module: + self.module = module + if funcdict: + self.dict = funcdict + # check existence required attributes + assert hasattr(self, 'name') + if not hasattr(self, 'signature'): + raise TypeError('You are decorating a non function: %s' % func) + + def update(self, func, **kw): + "Update the signature of func with the data in self" + func.__name__ = self.name + func.__doc__ = getattr(self, 'doc', None) + func.__dict__ = getattr(self, 'dict', {}) + func.__defaults__ = self.defaults + func.__kwdefaults__ = self.kwonlydefaults or None + func.__annotations__ = getattr(self, 'annotations', None) + try: + frame = sys._getframe(3) + except AttributeError: # for IronPython and similar implementations + callermodule = '?' + else: + callermodule = frame.f_globals.get('__name__', '?') + func.__module__ = getattr(self, 'module', callermodule) + func.__dict__.update(kw) + + def make(self, src_templ, evaldict=None, addsource=False, **attrs): + "Make a new function from a given template and update the signature" + src = src_templ % vars(self) # expand name and signature + evaldict = evaldict or {} + mo = DEF.search(src) + if mo is None: + raise SyntaxError('not a valid function template\n%s' % src) + name = mo.group(1) # extract the function name + names = set([name] + [arg.strip(' *') for arg in + self.shortsignature.split(',')]) + for n in names: + if n in ('_func_', '_call_'): + raise NameError('%s is overridden in\n%s' % (n, src)) + + if not src.endswith('\n'): # add a newline for old Pythons + src += '\n' + + # Ensure each generated function has a unique filename for profilers + # (such as cProfile) that depend on the tuple of (, + # , ) being unique. + filename = '' % (next(self._compile_count),) + try: + code = compile(src, filename, 'single') + exec(code, evaldict) + except Exception: + print('Error in generated code:', file=sys.stderr) + print(src, file=sys.stderr) + raise + func = evaldict[name] + if addsource: + attrs['__source__'] = src + self.update(func, **attrs) + return func + + @classmethod + def create(cls, obj, body, evaldict, defaults=None, + doc=None, module=None, addsource=True, **attrs): + """ + Create a function from the strings name, signature and body. + evaldict is the evaluation dictionary. If addsource is true an + attribute __source__ is added to the result. The attributes attrs + are added, if any. + """ + if isinstance(obj, str): # "name(signature)" + name, rest = obj.strip().split('(', 1) + signature = rest[:-1] # strip a right parens + func = None + else: # a function + name = None + signature = None + func = obj + self = cls(func, name, signature, defaults, doc, module) + ibody = '\n'.join(' ' + line for line in body.splitlines()) + caller = evaldict.get('_call_') # when called from `decorate` + if caller and iscoroutinefunction(caller): + body = ('async def %(name)s(%(signature)s):\n' + ibody).replace( + 'return', 'return await') + else: + body = 'def %(name)s(%(signature)s):\n' + ibody + return self.make(body, evaldict, addsource, **attrs) + + +def decorate(func, caller, extras=()): + """ + decorate(func, caller) decorates a function using a caller. + """ + evaldict = dict(_call_=caller, _func_=func) + es = '' + for i, extra in enumerate(extras): + ex = '_e%d_' % i + evaldict[ex] = extra + es += ex + ', ' + fun = FunctionMaker.create( + func, "return _call_(_func_, %s%%(shortsignature)s)" % es, + evaldict, __wrapped__=func) + if hasattr(func, '__qualname__'): + fun.__qualname__ = func.__qualname__ + return fun + + +def decorator(caller, _func=None): + """decorator(caller) converts a caller function into a decorator""" + if _func is not None: # return a decorated function + # this is obsolete behavior; you should use decorate instead + return decorate(_func, caller) + # else return a decorator function + defaultargs, defaults = '', () + if inspect.isclass(caller): + name = caller.__name__.lower() + doc = 'decorator(%s) converts functions/generators into ' \ + 'factories of %s objects' % (caller.__name__, caller.__name__) + elif inspect.isfunction(caller): + if caller.__name__ == '': + name = '_lambda_' + else: + name = caller.__name__ + doc = caller.__doc__ + nargs = caller.__code__.co_argcount + ndefs = len(caller.__defaults__ or ()) + defaultargs = ', '.join(caller.__code__.co_varnames[nargs-ndefs:nargs]) + if defaultargs: + defaultargs += ',' + defaults = caller.__defaults__ + else: # assume caller is an object with a __call__ method + name = caller.__class__.__name__.lower() + doc = caller.__call__.__doc__ + evaldict = dict(_call=caller, _decorate_=decorate) + dec = FunctionMaker.create( + '%s(%s func)' % (name, defaultargs), + 'if func is None: return lambda func: _decorate_(func, _call, (%s))\n' + 'return _decorate_(func, _call, (%s))' % (defaultargs, defaultargs), + evaldict, doc=doc, module=caller.__module__, __wrapped__=caller) + if defaults: + dec.__defaults__ = defaults + (None,) + return dec + + +# ####################### contextmanager ####################### # + +try: # Python >= 3.2 + from contextlib import _GeneratorContextManager +except ImportError: # Python >= 2.5 + from contextlib import GeneratorContextManager as _GeneratorContextManager + + +class ContextManager(_GeneratorContextManager): + def __call__(self, func): + """Context manager decorator""" + return FunctionMaker.create( + func, "with _self_: return _func_(%(shortsignature)s)", + dict(_self_=self, _func_=func), __wrapped__=func) + + +init = getfullargspec(_GeneratorContextManager.__init__) +n_args = len(init.args) +if n_args == 2 and not init.varargs: # (self, genobj) Python 2.7 + def __init__(self, g, *a, **k): + return _GeneratorContextManager.__init__(self, g(*a, **k)) + ContextManager.__init__ = __init__ +elif n_args == 2 and init.varargs: # (self, gen, *a, **k) Python 3.4 + pass +elif n_args == 4: # (self, gen, args, kwds) Python 3.5 + def __init__(self, g, *a, **k): + return _GeneratorContextManager.__init__(self, g, a, k) + ContextManager.__init__ = __init__ + +_contextmanager = decorator(ContextManager) + + +def contextmanager(func): + # Enable Pylint config: contextmanager-decorators=decorator.contextmanager + return _contextmanager(func) + + +# ############################ dispatch_on ############################ # + +def append(a, vancestors): + """ + Append ``a`` to the list of the virtual ancestors, unless it is already + included. + """ + add = True + for j, va in enumerate(vancestors): + if issubclass(va, a): + add = False + break + if issubclass(a, va): + vancestors[j] = a + add = False + if add: + vancestors.append(a) + + +# inspired from simplegeneric by P.J. Eby and functools.singledispatch +def dispatch_on(*dispatch_args): + """ + Factory of decorators turning a function into a generic function + dispatching on the given arguments. + """ + assert dispatch_args, 'No dispatch args passed' + dispatch_str = '(%s,)' % ', '.join(dispatch_args) + + def check(arguments, wrong=operator.ne, msg=''): + """Make sure one passes the expected number of arguments""" + if wrong(len(arguments), len(dispatch_args)): + raise TypeError('Expected %d arguments, got %d%s' % + (len(dispatch_args), len(arguments), msg)) + + def gen_func_dec(func): + """Decorator turning a function into a generic function""" + + # first check the dispatch arguments + argset = set(getfullargspec(func).args) + if not set(dispatch_args) <= argset: + raise NameError('Unknown dispatch arguments %s' % dispatch_str) + + typemap = {} + + def vancestors(*types): + """ + Get a list of sets of virtual ancestors for the given types + """ + check(types) + ras = [[] for _ in range(len(dispatch_args))] + for types_ in typemap: + for t, type_, ra in zip(types, types_, ras): + if issubclass(t, type_) and type_ not in t.mro(): + append(type_, ra) + return [set(ra) for ra in ras] + + def ancestors(*types): + """ + Get a list of virtual MROs, one for each type + """ + check(types) + lists = [] + for t, vas in zip(types, vancestors(*types)): + n_vas = len(vas) + if n_vas > 1: + raise RuntimeError( + 'Ambiguous dispatch for %s: %s' % (t, vas)) + elif n_vas == 1: + va, = vas + mro = type('t', (t, va), {}).mro()[1:] + else: + mro = t.mro() + lists.append(mro[:-1]) # discard t and object + return lists + + def register(*types): + """ + Decorator to register an implementation for the given types + """ + check(types) + + def dec(f): + check(getfullargspec(f).args, operator.lt, ' in ' + f.__name__) + typemap[types] = f + return f + return dec + + def dispatch_info(*types): + """ + An utility to introspect the dispatch algorithm + """ + check(types) + lst = [] + for anc in itertools.product(*ancestors(*types)): + lst.append(tuple(a.__name__ for a in anc)) + return lst + + def _dispatch(dispatch_args, *args, **kw): + types = tuple(type(arg) for arg in dispatch_args) + try: # fast path + f = typemap[types] + except KeyError: + pass + else: + return f(*args, **kw) + combinations = itertools.product(*ancestors(*types)) + next(combinations) # the first one has been already tried + for types_ in combinations: + f = typemap.get(types_) + if f is not None: + return f(*args, **kw) + + # else call the default implementation + return func(*args, **kw) + + return FunctionMaker.create( + func, 'return _f_(%s, %%(shortsignature)s)' % dispatch_str, + dict(_f_=_dispatch), register=register, default=func, + typemap=typemap, vancestors=vancestors, ancestors=ancestors, + dispatch_info=dispatch_info, __wrapped__=func) + + gen_func_dec.__name__ = 'dispatch_on' + dispatch_str + return gen_func_dec diff --git a/libs/dogpile/__init__.py b/libs/dogpile/__init__.py index f48ad105..fc8fd452 100644 --- a/libs/dogpile/__init__.py +++ b/libs/dogpile/__init__.py @@ -1,6 +1,4 @@ -# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages -try: - __import__('pkg_resources').declare_namespace(__name__) -except ImportError: - from pkgutil import extend_path - __path__ = extend_path(__path__, __name__) +__version__ = '0.7.1' + +from .lock import Lock # noqa +from .lock import NeedRegenerationException # noqa diff --git a/libs/dogpile/cache/__init__.py b/libs/dogpile/cache/__init__.py index 7cd49eaa..fb57cbcc 100644 --- a/libs/dogpile/cache/__init__.py +++ b/libs/dogpile/cache/__init__.py @@ -1,3 +1,4 @@ -__version__ = '0.5.4' +from .region import CacheRegion, register_backend, make_region # noqa -from .region import CacheRegion, register_backend, make_region +# backwards compat +from .. import __version__ # noqa diff --git a/libs/dogpile/cache/api.py b/libs/dogpile/cache/api.py index 4c5be493..d66e5a70 100644 --- a/libs/dogpile/cache/api.py +++ b/libs/dogpile/cache/api.py @@ -1,5 +1,5 @@ import operator -from .compat import py3k +from ..util.compat import py3k class NoValue(object): @@ -13,17 +13,26 @@ class NoValue(object): def payload(self): return self + def __repr__(self): + """Ensure __repr__ is a consistent value in case NoValue is used to + fill another cache key. + + """ + return '' + if py3k: - def __bool__(self): #pragma NO COVERAGE + def __bool__(self): # pragma NO COVERAGE return False else: - def __nonzero__(self): #pragma NO COVERAGE + def __nonzero__(self): # pragma NO COVERAGE return False + NO_VALUE = NoValue() """Value returned from ``get()`` that describes a key not present.""" + class CachedValue(tuple): """Represent a value stored in the cache. @@ -47,6 +56,7 @@ class CachedValue(tuple): def __reduce__(self): return CachedValue, (self.payload, self.metadata) + class CacheBackend(object): """Base class for backend implementations.""" @@ -58,7 +68,7 @@ class CacheBackend(object): """ - def __init__(self, arguments): #pragma NO COVERAGE + def __init__(self, arguments): # pragma NO COVERAGE """Construct a new :class:`.CacheBackend`. Subclasses should override this to @@ -74,12 +84,15 @@ class CacheBackend(object): def from_config_dict(cls, config_dict, prefix): prefix_len = len(prefix) return cls( - dict( - (key[prefix_len:], config_dict[key]) - for key in config_dict - if key.startswith(prefix) - ) + dict( + (key[prefix_len:], config_dict[key]) + for key in config_dict + if key.startswith(prefix) ) + ) + + def has_lock_timeout(self): + return False def get_mutex(self, key): """Return an optional mutexing object for the given key. @@ -114,7 +127,7 @@ class CacheBackend(object): """ return None - def get(self, key): #pragma NO COVERAGE + def get(self, key): # pragma NO COVERAGE """Retrieve a value from the cache. The returned value should be an instance of @@ -124,7 +137,7 @@ class CacheBackend(object): """ raise NotImplementedError() - def get_multi(self, keys): #pragma NO COVERAGE + def get_multi(self, keys): # pragma NO COVERAGE """Retrieve multiple values from the cache. The returned value should be a list, corresponding @@ -135,7 +148,7 @@ class CacheBackend(object): """ raise NotImplementedError() - def set(self, key, value): #pragma NO COVERAGE + def set(self, key, value): # pragma NO COVERAGE """Set a value in the cache. The key will be whatever was passed @@ -147,21 +160,30 @@ class CacheBackend(object): """ raise NotImplementedError() - def set_multi(self, mapping): #pragma NO COVERAGE + def set_multi(self, mapping): # pragma NO COVERAGE """Set multiple values in the cache. - The key will be whatever was passed + ``mapping`` is a dict in which + the key will be whatever was passed to the registry, processed by the "key mangling" function, if any. The value will always be an instance of :class:`.CachedValue`. + When implementing a new :class:`.CacheBackend` or cutomizing via + :class:`.ProxyBackend`, be aware that when this method is invoked by + :meth:`.Region.get_or_create_multi`, the ``mapping`` values are the + same ones returned to the upstream caller. If the subclass alters the + values in any way, it must not do so 'in-place' on the ``mapping`` dict + -- that will have the undesirable effect of modifying the returned + values as well. + .. versionadded:: 0.5.0 """ raise NotImplementedError() - def delete(self, key): #pragma NO COVERAGE + def delete(self, key): # pragma NO COVERAGE """Delete a value from the cache. The key will be whatever was passed @@ -175,7 +197,7 @@ class CacheBackend(object): """ raise NotImplementedError() - def delete_multi(self, keys): #pragma NO COVERAGE + def delete_multi(self, keys): # pragma NO COVERAGE """Delete multiple values from the cache. The key will be whatever was passed diff --git a/libs/dogpile/cache/backends/__init__.py b/libs/dogpile/cache/backends/__init__.py index 84c31abe..041f05a3 100644 --- a/libs/dogpile/cache/backends/__init__.py +++ b/libs/dogpile/cache/backends/__init__.py @@ -1,10 +1,22 @@ from dogpile.cache.region import register_backend -register_backend("dogpile.cache.null", "dogpile.cache.backends.null", "NullBackend") -register_backend("dogpile.cache.dbm", "dogpile.cache.backends.file", "DBMBackend") -register_backend("dogpile.cache.pylibmc", "dogpile.cache.backends.memcached", "PylibmcBackend") -register_backend("dogpile.cache.bmemcached", "dogpile.cache.backends.memcached", "BMemcachedBackend") -register_backend("dogpile.cache.memcached", "dogpile.cache.backends.memcached", "MemcachedBackend") -register_backend("dogpile.cache.memory", "dogpile.cache.backends.memory", "MemoryBackend") -register_backend("dogpile.cache.memory_pickle", "dogpile.cache.backends.memory", "MemoryPickleBackend") -register_backend("dogpile.cache.redis", "dogpile.cache.backends.redis", "RedisBackend") +register_backend( + "dogpile.cache.null", "dogpile.cache.backends.null", "NullBackend") +register_backend( + "dogpile.cache.dbm", "dogpile.cache.backends.file", "DBMBackend") +register_backend( + "dogpile.cache.pylibmc", "dogpile.cache.backends.memcached", + "PylibmcBackend") +register_backend( + "dogpile.cache.bmemcached", "dogpile.cache.backends.memcached", + "BMemcachedBackend") +register_backend( + "dogpile.cache.memcached", "dogpile.cache.backends.memcached", + "MemcachedBackend") +register_backend( + "dogpile.cache.memory", "dogpile.cache.backends.memory", "MemoryBackend") +register_backend( + "dogpile.cache.memory_pickle", "dogpile.cache.backends.memory", + "MemoryPickleBackend") +register_backend( + "dogpile.cache.redis", "dogpile.cache.backends.redis", "RedisBackend") diff --git a/libs/dogpile/cache/backends/file.py b/libs/dogpile/cache/backends/file.py index 5d2d2e96..309c055a 100644 --- a/libs/dogpile/cache/backends/file.py +++ b/libs/dogpile/cache/backends/file.py @@ -7,14 +7,15 @@ Provides backends that deal with local filesystem access. """ from __future__ import with_statement -from dogpile.cache.api import CacheBackend, NO_VALUE +from ..api import CacheBackend, NO_VALUE from contextlib import contextmanager -from dogpile.cache import compat -from dogpile.cache import util +from ...util import compat +from ... import util import os __all__ = 'DBMBackend', 'FileLock', 'AbstractFileLock' + class DBMBackend(CacheBackend): """A file-backend using a dbm file to store keys. @@ -135,19 +136,19 @@ class DBMBackend(CacheBackend): """ def __init__(self, arguments): self.filename = os.path.abspath( - os.path.normpath(arguments['filename']) - ) + os.path.normpath(arguments['filename']) + ) dir_, filename = os.path.split(self.filename) self.lock_factory = arguments.get("lock_factory", FileLock) self._rw_lock = self._init_lock( - arguments.get('rw_lockfile'), - ".rw.lock", dir_, filename) + arguments.get('rw_lockfile'), + ".rw.lock", dir_, filename) self._dogpile_lock = self._init_lock( - arguments.get('dogpile_lockfile'), - ".dogpile.lock", - dir_, filename, - util.KeyReentrantMutex.factory) + arguments.get('dogpile_lockfile'), + ".dogpile.lock", + dir_, filename, + util.KeyReentrantMutex.factory) # TODO: make this configurable if compat.py3k: @@ -162,9 +163,9 @@ class DBMBackend(CacheBackend): lock = self.lock_factory(os.path.join(basedir, basefile + suffix)) elif argument is not False: lock = self.lock_factory( - os.path.abspath( - os.path.normpath(argument) - )) + os.path.abspath( + os.path.normpath(argument) + )) else: return None if wrapper: @@ -209,8 +210,9 @@ class DBMBackend(CacheBackend): @contextmanager def _dbm_file(self, write): with self._use_rw_lock(write): - dbm = self.dbmmodule.open(self.filename, - "w" if write else "r") + dbm = self.dbmmodule.open( + self.filename, + "w" if write else "r") yield dbm dbm.close() @@ -233,12 +235,14 @@ class DBMBackend(CacheBackend): def set(self, key, value): with self._dbm_file(True) as dbm: - dbm[key] = compat.pickle.dumps(value) + dbm[key] = compat.pickle.dumps(value, + compat.pickle.HIGHEST_PROTOCOL) def set_multi(self, mapping): with self._dbm_file(True) as dbm: - for key,value in mapping.items(): - dbm[key] = compat.pickle.dumps(value) + for key, value in mapping.items(): + dbm[key] = compat.pickle.dumps(value, + compat.pickle.HIGHEST_PROTOCOL) def delete(self, key): with self._dbm_file(True) as dbm: @@ -255,6 +259,7 @@ class DBMBackend(CacheBackend): except KeyError: pass + class AbstractFileLock(object): """Coordinate read/write access to a file. @@ -275,10 +280,10 @@ class AbstractFileLock(object): file. Note that multithreaded environments must provide a thread-safe - version of this lock. The recommended approach for file-descriptor-based - locks is to use a Python ``threading.local()`` so that a unique file descriptor - is held per thread. See the source code of :class:`.FileLock` for an - implementation example. + version of this lock. The recommended approach for file- + descriptor-based locks is to use a Python ``threading.local()`` so + that a unique file descriptor is held per thread. See the source + code of :class:`.FileLock` for an implementation example. """ @@ -377,6 +382,7 @@ class AbstractFileLock(object): """ raise NotImplementedError() + class FileLock(AbstractFileLock): """Use lockfiles to coordinate read/write access to a file. diff --git a/libs/dogpile/cache/backends/memcached.py b/libs/dogpile/cache/backends/memcached.py index 03943504..6758a998 100644 --- a/libs/dogpile/cache/backends/memcached.py +++ b/libs/dogpile/cache/backends/memcached.py @@ -6,15 +6,16 @@ Provides backends for talking to `memcached `_. """ -from dogpile.cache.api import CacheBackend, NO_VALUE -from dogpile.cache import compat -from dogpile.cache import util +from ..api import CacheBackend, NO_VALUE +from ...util import compat +from ... import util import random import time __all__ = 'GenericMemcachedBackend', 'MemcachedBackend',\ 'PylibmcBackend', 'BMemcachedBackend', 'MemcachedLock' + class MemcachedLock(object): """Simple distributed lock using memcached. @@ -23,20 +24,21 @@ class MemcachedLock(object): """ - def __init__(self, client_fn, key): + def __init__(self, client_fn, key, timeout=0): self.client_fn = client_fn self.key = "_lock" + key + self.timeout = timeout def acquire(self, wait=True): client = self.client_fn() i = 0 while True: - if client.add(self.key, 1): + if client.add(self.key, 1, self.timeout): return True elif not wait: return False else: - sleep_time = (((i+1)*random.random()) + 2**i) / 2.5 + sleep_time = (((i + 1) * random.random()) + 2 ** i) / 2.5 time.sleep(sleep_time) if i < 15: i += 1 @@ -45,6 +47,7 @@ class MemcachedLock(object): client = self.client_fn() client.delete(self.key) + class GenericMemcachedBackend(CacheBackend): """Base class for memcached backends. @@ -60,6 +63,12 @@ class GenericMemcachedBackend(CacheBackend): processes will be talking to the same memcached instance. When left at False, dogpile will coordinate on a regular threading mutex. + :param lock_timeout: integer, number of seconds after acquiring a lock that + memcached should expire it. This argument is only valid when + ``distributed_lock`` is ``True``. + + .. versionadded:: 0.5.7 + :param memcached_expire_time: integer, when present will be passed as the ``time`` parameter to ``pylibmc.Client.set``. This is used to set the memcached expiry time for a value. @@ -104,9 +113,13 @@ class GenericMemcachedBackend(CacheBackend): # automatically. self.url = util.to_list(arguments['url']) self.distributed_lock = arguments.get('distributed_lock', False) + self.lock_timeout = arguments.get('lock_timeout', 0) self.memcached_expire_time = arguments.get( 'memcached_expire_time', 0) + def has_lock_timeout(self): + return self.lock_timeout != 0 + def _imports(self): """client library imports go here.""" raise NotImplementedError() @@ -118,6 +131,7 @@ class GenericMemcachedBackend(CacheBackend): @util.memoized_property def _clients(self): backend = self + class ClientPool(compat.threading.local): def __init__(self): self.memcached = backend._create_client() @@ -138,7 +152,8 @@ class GenericMemcachedBackend(CacheBackend): def get_mutex(self, key): if self.distributed_lock: - return MemcachedLock(lambda: self.client, key) + return MemcachedLock(lambda: self.client, key, + timeout=self.lock_timeout) else: return None @@ -157,13 +172,15 @@ class GenericMemcachedBackend(CacheBackend): ] def set(self, key, value): - self.client.set(key, + self.client.set( + key, value, **self.set_arguments ) def set_multi(self, mapping): - self.client.set_multi(mapping, + self.client.set_multi( + mapping, **self.set_arguments ) @@ -173,6 +190,7 @@ class GenericMemcachedBackend(CacheBackend): def delete_multi(self, keys): self.client.delete_multi(keys) + class MemcacheArgs(object): """Mixin which provides support for the 'time' argument to set(), 'min_compress_len' to other methods. @@ -183,13 +201,15 @@ class MemcacheArgs(object): self.set_arguments = {} if "memcached_expire_time" in arguments: - self.set_arguments["time"] =\ - arguments["memcached_expire_time"] + self.set_arguments["time"] = arguments["memcached_expire_time"] if "min_compress_len" in arguments: - self.set_arguments["min_compress_len"] =\ - arguments["min_compress_len"] + self.set_arguments["min_compress_len"] = \ + arguments["min_compress_len"] super(MemcacheArgs, self).__init__(arguments) +pylibmc = None + + class PylibmcBackend(MemcacheArgs, GenericMemcachedBackend): """A backend for the `pylibmc `_ @@ -229,19 +249,24 @@ class PylibmcBackend(MemcacheArgs, GenericMemcachedBackend): self.behaviors = arguments.get('behaviors', {}) super(PylibmcBackend, self).__init__(arguments) - def _imports(self): global pylibmc - import pylibmc + import pylibmc # noqa def _create_client(self): - return pylibmc.Client(self.url, + return pylibmc.Client( + self.url, binary=self.binary, behaviors=self.behaviors ) +memcache = None + + class MemcachedBackend(MemcacheArgs, GenericMemcachedBackend): - """A backend using the standard `Python-memcached `_ + """A backend using the standard + `Python-memcached `_ library. Example:: @@ -259,14 +284,19 @@ class MemcachedBackend(MemcacheArgs, GenericMemcachedBackend): """ def _imports(self): global memcache - import memcache + import memcache # noqa def _create_client(self): return memcache.Client(self.url) + +bmemcached = None + + class BMemcachedBackend(GenericMemcachedBackend): """A backend for the - `python-binary-memcached `_ + `python-binary-memcached `_ memcached client. This is a pure Python memcached client which @@ -312,16 +342,18 @@ class BMemcachedBackend(GenericMemcachedBackend): """ - def add(self, key, value): + def add(self, key, value, timeout=0): try: - return super(RepairBMemcachedAPI, self).add(key, value) + return super(RepairBMemcachedAPI, self).add( + key, value, timeout) except ValueError: return False self.Client = RepairBMemcachedAPI def _create_client(self): - return self.Client(self.url, + return self.Client( + self.url, username=self.username, password=self.password ) diff --git a/libs/dogpile/cache/backends/memory.py b/libs/dogpile/cache/backends/memory.py index cee989bc..e2083f7f 100644 --- a/libs/dogpile/cache/backends/memory.py +++ b/libs/dogpile/cache/backends/memory.py @@ -10,8 +10,9 @@ places the value as given into the dictionary. """ -from dogpile.cache.api import CacheBackend, NO_VALUE -from dogpile.cache.compat import pickle +from ..api import CacheBackend, NO_VALUE +from ...util.compat import pickle + class MemoryBackend(CacheBackend): """A backend that uses a plain dictionary. @@ -58,14 +59,15 @@ class MemoryBackend(CacheBackend): return value def get_multi(self, keys): - ret = [self._cache.get(key, NO_VALUE) + ret = [ + self._cache.get(key, NO_VALUE) for key in keys] if self.pickle_values: ret = [ - pickle.loads(value) - if value is not NO_VALUE else value - for value in ret - ] + pickle.loads(value) + if value is not NO_VALUE else value + for value in ret + ] return ret def set(self, key, value): diff --git a/libs/dogpile/cache/backends/null.py b/libs/dogpile/cache/backends/null.py index c1f46a9d..603cca3f 100644 --- a/libs/dogpile/cache/backends/null.py +++ b/libs/dogpile/cache/backends/null.py @@ -10,15 +10,15 @@ caching for a region that is otherwise used normally. """ -from dogpile.cache.api import CacheBackend, NO_VALUE +from ..api import CacheBackend, NO_VALUE __all__ = ['NullBackend'] class NullLock(object): - def acquire(self): - pass + def acquire(self, wait=True): + return True def release(self): pass diff --git a/libs/dogpile/cache/backends/redis.py b/libs/dogpile/cache/backends/redis.py index 365bdbc7..d665320a 100644 --- a/libs/dogpile/cache/backends/redis.py +++ b/libs/dogpile/cache/backends/redis.py @@ -7,8 +7,8 @@ Provides backends for talking to `Redis `_. """ from __future__ import absolute_import -from dogpile.cache.api import CacheBackend, NO_VALUE -from dogpile.cache.compat import pickle, u +from ..api import CacheBackend, NO_VALUE +from ...util.compat import pickle, u redis = None @@ -30,7 +30,7 @@ class RedisBackend(CacheBackend): 'port': 6379, 'db': 0, 'redis_expiration_time': 60*60*2, # 2 hours - 'distributed_lock':True + 'distributed_lock': True } ) @@ -91,6 +91,7 @@ class RedisBackend(CacheBackend): """ def __init__(self, arguments): + arguments = arguments.copy() self._imports() self.url = arguments.pop('url', None) self.host = arguments.pop('host', 'localhost') @@ -110,7 +111,7 @@ class RedisBackend(CacheBackend): def _imports(self): # defer imports until backend is used global redis - import redis + import redis # noqa def _create_client(self): if self.connection_pool is not None: @@ -133,7 +134,6 @@ class RedisBackend(CacheBackend): ) return redis.StrictRedis(**args) - def get_mutex(self, key): if self.distributed_lock: return self.client.lock(u('_lock{0}').format(key), @@ -148,9 +148,12 @@ class RedisBackend(CacheBackend): return pickle.loads(value) def get_multi(self, keys): + if not keys: + return [] values = self.client.mget(keys) - return [pickle.loads(v) if v is not None else NO_VALUE - for v in values] + return [ + pickle.loads(v) if v is not None else NO_VALUE + for v in values] def set(self, key, value): if self.redis_expiration_time: @@ -178,4 +181,3 @@ class RedisBackend(CacheBackend): def delete_multi(self, keys): self.client.delete(*keys) - diff --git a/libs/dogpile/cache/exception.py b/libs/dogpile/cache/exception.py index 58068f8b..adeb1b4a 100644 --- a/libs/dogpile/cache/exception.py +++ b/libs/dogpile/cache/exception.py @@ -15,3 +15,11 @@ class RegionNotConfigured(DogpileCacheException): class ValidationError(DogpileCacheException): """Error validating a value or option.""" + + +class PluginNotFound(DogpileCacheException): + """The specified plugin could not be found. + + .. versionadded:: 0.6.4 + + """ diff --git a/libs/dogpile/cache/plugins/mako_cache.py b/libs/dogpile/cache/plugins/mako_cache.py index a1544d62..61f4ffaf 100644 --- a/libs/dogpile/cache/plugins/mako_cache.py +++ b/libs/dogpile/cache/plugins/mako_cache.py @@ -2,7 +2,8 @@ Mako Integration ---------------- -dogpile.cache includes a `Mako `_ plugin that replaces `Beaker `_ +dogpile.cache includes a `Mako `_ plugin +that replaces `Beaker `_ as the cache backend. Setup a Mako template lookup using the "dogpile.cache" cache implementation and a region dictionary:: @@ -31,9 +32,9 @@ and a region dictionary:: } ) -To use the above configuration in a template, use the ``cached=True`` argument on any -Mako tag which accepts it, in conjunction with the name of the desired region -as the ``cache_region`` argument:: +To use the above configuration in a template, use the ``cached=True`` +argument on any Mako tag which accepts it, in conjunction with the +name of the desired region as the ``cache_region`` argument:: <%def name="mysection()" cached="True" cache_region="memcached"> some content that's cached @@ -43,6 +44,7 @@ as the ``cache_region`` argument:: """ from mako.cache import CacheImpl + class MakoPlugin(CacheImpl): """A Mako ``CacheImpl`` which talks to dogpile.cache.""" @@ -70,8 +72,9 @@ class MakoPlugin(CacheImpl): def get_and_replace(self, key, creation_function, **kw): expiration_time = kw.pop("timeout", None) - return self._get_region(**kw).get_or_create(key, creation_function, - expiration_time=expiration_time) + return self._get_region(**kw).get_or_create( + key, creation_function, + expiration_time=expiration_time) def get_or_create(self, key, creation_function, **kw): return self.get_and_replace(key, creation_function, **kw) diff --git a/libs/dogpile/cache/proxy.py b/libs/dogpile/cache/proxy.py index c1518d21..15c6b574 100644 --- a/libs/dogpile/cache/proxy.py +++ b/libs/dogpile/cache/proxy.py @@ -12,6 +12,7 @@ base backend. from .api import CacheBackend + class ProxyBackend(CacheBackend): """A decorator class for altering the functionality of backends. @@ -62,7 +63,9 @@ class ProxyBackend(CacheBackend): Return an object that be used as a backend by a :class:`.CacheRegion` object. ''' - assert(isinstance(backend, CacheBackend) or isinstance(backend, ProxyBackend)) + assert( + isinstance(backend, CacheBackend) or + isinstance(backend, ProxyBackend)) self.proxied = backend return self @@ -82,12 +85,11 @@ class ProxyBackend(CacheBackend): def get_multi(self, keys): return self.proxied.get_multi(keys) - def set_multi(self, keys): - self.proxied.set_multi(keys) + def set_multi(self, mapping): + self.proxied.set_multi(mapping) def delete_multi(self, keys): self.proxied.delete_multi(keys) def get_mutex(self, key): return self.proxied.get_mutex(key) - diff --git a/libs/dogpile/cache/region.py b/libs/dogpile/cache/region.py index 39cfd0aa..261a8db4 100644 --- a/libs/dogpile/cache/region.py +++ b/libs/dogpile/cache/region.py @@ -1,21 +1,22 @@ from __future__ import with_statement -from dogpile.core import Lock, NeedRegenerationException -from dogpile.core.nameregistry import NameRegistry +from .. import Lock, NeedRegenerationException +from ..util import NameRegistry from . import exception -from .util import function_key_generator, PluginLoader, \ - memoized_property, coerce_string_conf, function_multi_key_generator +from ..util import PluginLoader, memoized_property, coerce_string_conf +from .util import function_key_generator, function_multi_key_generator from .api import NO_VALUE, CachedValue from .proxy import ProxyBackend -from . import compat +from ..util import compat import time import datetime from numbers import Number -from functools import wraps +from functools import wraps, partial import threading +from decorator import decorate _backend_loader = PluginLoader("dogpile.cache") register_backend = _backend_loader.register -from . import backends +from . import backends # noqa value_version = 1 """An integer placed in the :class:`.CachedValue` @@ -24,8 +25,171 @@ values from a previous, backwards-incompatible version. """ + +class RegionInvalidationStrategy(object): + """Region invalidation strategy interface + + Implement this interface and pass implementation instance + to :meth:`.CacheRegion.configure` to override default region invalidation. + + Example:: + + class CustomInvalidationStrategy(RegionInvalidationStrategy): + + def __init__(self): + self._soft_invalidated = None + self._hard_invalidated = None + + def invalidate(self, hard=None): + if hard: + self._soft_invalidated = None + self._hard_invalidated = time.time() + else: + self._soft_invalidated = time.time() + self._hard_invalidated = None + + def is_invalidated(self, timestamp): + return ((self._soft_invalidated and + timestamp < self._soft_invalidated) or + (self._hard_invalidated and + timestamp < self._hard_invalidated)) + + def was_hard_invalidated(self): + return bool(self._hard_invalidated) + + def is_hard_invalidated(self, timestamp): + return (self._hard_invalidated and + timestamp < self._hard_invalidated) + + def was_soft_invalidated(self): + return bool(self._soft_invalidated) + + def is_soft_invalidated(self, timestamp): + return (self._soft_invalidated and + timestamp < self._soft_invalidated) + + The custom implementation is injected into a :class:`.CacheRegion` + at configure time using the + :paramref:`.CacheRegion.configure.region_invalidator` parameter:: + + region = CacheRegion() + + region = region.configure(region_invalidator=CustomInvalidationStrategy()) + + Invalidation strategies that wish to have access to the + :class:`.CacheRegion` itself should construct the invalidator given the + region as an argument:: + + class MyInvalidator(RegionInvalidationStrategy): + def __init__(self, region): + self.region = region + # ... + + # ... + + region = CacheRegion() + region = region.configure(region_invalidator=MyInvalidator(region)) + + .. versionadded:: 0.6.2 + + .. seealso:: + + :paramref:`.CacheRegion.configure.region_invalidator` + + """ + + def invalidate(self, hard=True): + """Region invalidation. + + :class:`.CacheRegion` propagated call. + The default invalidation system works by setting + a current timestamp (using ``time.time()``) to consider all older + timestamps effectively invalidated. + + """ + + raise NotImplementedError() + + def is_hard_invalidated(self, timestamp): + """Check timestamp to determine if it was hard invalidated. + + :return: Boolean. True if ``timestamp`` is older than + the last region invalidation time and region is invalidated + in hard mode. + + """ + + raise NotImplementedError() + + def is_soft_invalidated(self, timestamp): + """Check timestamp to determine if it was soft invalidated. + + :return: Boolean. True if ``timestamp`` is older than + the last region invalidation time and region is invalidated + in soft mode. + + """ + + raise NotImplementedError() + + def is_invalidated(self, timestamp): + """Check timestamp to determine if it was invalidated. + + :return: Boolean. True if ``timestamp`` is older than + the last region invalidation time. + + """ + + raise NotImplementedError() + + def was_soft_invalidated(self): + """Indicate the region was invalidated in soft mode. + + :return: Boolean. True if region was invalidated in soft mode. + + """ + + raise NotImplementedError() + + def was_hard_invalidated(self): + """Indicate the region was invalidated in hard mode. + + :return: Boolean. True if region was invalidated in hard mode. + + """ + + raise NotImplementedError() + + +class DefaultInvalidationStrategy(RegionInvalidationStrategy): + + def __init__(self): + self._is_hard_invalidated = None + self._invalidated = None + + def invalidate(self, hard=True): + self._is_hard_invalidated = bool(hard) + self._invalidated = time.time() + + def is_invalidated(self, timestamp): + return (self._invalidated is not None and + timestamp < self._invalidated) + + def was_hard_invalidated(self): + return self._is_hard_invalidated is True + + def is_hard_invalidated(self, timestamp): + return self.was_hard_invalidated() and self.is_invalidated(timestamp) + + def was_soft_invalidated(self): + return self._is_hard_invalidated is False + + def is_soft_invalidated(self, timestamp): + return self.was_soft_invalidated() and self.is_invalidated(timestamp) + + class CacheRegion(object): - """A front end to a particular cache backend. + r"""A front end to a particular cache backend. :param name: Optional, a string name for the region. This isn't used internally @@ -78,6 +242,13 @@ class CacheRegion(object): def my_function(a, b, **kw): return my_data() + .. seealso:: + + :func:`.function_key_generator` - default key generator + + :func:`.kwarg_function_key_generator` - optional gen that also + uses keyword arguments + :param function_multi_key_generator: Optional. Similar to ``function_key_generator`` parameter, but it's used in :meth:`.CacheRegion.cache_multi_on_arguments`. Generated function @@ -156,7 +327,8 @@ class CacheRegion(object): """ - def __init__(self, + def __init__( + self, name=None, function_key_generator=function_key_generator, function_multi_key_generator=function_multi_key_generator, @@ -167,21 +339,20 @@ class CacheRegion(object): self.name = name self.function_key_generator = function_key_generator self.function_multi_key_generator = function_multi_key_generator - if key_mangler: - self.key_mangler = key_mangler - else: - self.key_mangler = None - self._hard_invalidated = None - self._soft_invalidated = None + self.key_mangler = self._user_defined_key_mangler = key_mangler self.async_creation_runner = async_creation_runner + self.region_invalidator = DefaultInvalidationStrategy() - def configure(self, backend, + def configure( + self, backend, expiration_time=None, arguments=None, _config_argument_dict=None, _config_prefix=None, - wrap=None - ): + wrap=None, + replace_existing_backend=False, + region_invalidator=None + ): """Configure a :class:`.CacheRegion`. The :class:`.CacheRegion` itself @@ -220,14 +391,33 @@ class CacheRegion(object): :ref:`changing_backend_behavior` + :param replace_existing_backend: if True, the existing cache backend + will be replaced. Without this flag, an exception is raised if + a backend is already configured. + + .. versionadded:: 0.5.7 + + :param region_invalidator: Optional. Override default invalidation + strategy with custom implementation of + :class:`.RegionInvalidationStrategy`. + + .. versionadded:: 0.6.2 + """ - if "backend" in self.__dict__: + if "backend" in self.__dict__ and not replace_existing_backend: raise exception.RegionAlreadyConfigured( - "This region is already " - "configured with backend: %s" - % self.backend) - backend_cls = _backend_loader.load(backend) + "This region is already " + "configured with backend: %s. " + "Specify replace_existing_backend=True to replace." + % self.backend) + + try: + backend_cls = _backend_loader.load(backend) + except PluginLoader.NotFound: + raise exception.PluginNotFound( + "Couldn't find cache plugin to load: %s" % backend) + if _config_argument_dict: self.backend = backend_cls.from_config_dict( _config_argument_dict, @@ -239,20 +429,24 @@ class CacheRegion(object): if not expiration_time or isinstance(expiration_time, Number): self.expiration_time = expiration_time elif isinstance(expiration_time, datetime.timedelta): - self.expiration_time = int(compat.timedelta_total_seconds(expiration_time)) + self.expiration_time = int( + compat.timedelta_total_seconds(expiration_time)) else: raise exception.ValidationError( 'expiration_time is not a number or timedelta.') - if self.key_mangler is None: + if not self._user_defined_key_mangler: self.key_mangler = self.backend.key_mangler self._lock_registry = NameRegistry(self._create_mutex) - if getattr(wrap,'__iter__', False): + if getattr(wrap, '__iter__', False): for wrapper in reversed(wrap): self.wrap(wrapper) + if region_invalidator: + self.region_invalidator = region_invalidator + return self def wrap(self, proxy): @@ -270,7 +464,6 @@ class CacheRegion(object): self.backend = proxy.wrap(self.backend) - def _mutex(self, key): return self._lock_registry.get(key) @@ -292,29 +485,60 @@ class CacheRegion(object): else: return self._LockWrapper() + # cached value + _actual_backend = None + + @property + def actual_backend(self): + """Return the ultimate backend underneath any proxies. + + The backend might be the result of one or more ``proxy.wrap`` + applications. If so, derive the actual underlying backend. + + .. versionadded:: 0.6.6 + + """ + if self._actual_backend is None: + _backend = self.backend + while hasattr(_backend, 'proxied'): + _backend = _backend.proxied + self._actual_backend = _backend + return self._actual_backend + def invalidate(self, hard=True): """Invalidate this :class:`.CacheRegion`. - Invalidation works by setting a current timestamp - (using ``time.time()``) + The default invalidation system works by setting + a current timestamp (using ``time.time()``) representing the "minimum creation time" for a value. Any retrieved value whose creation time is prior to this timestamp is considered to be stale. It does not - affect the data in the cache in any way, and is also - local to this instance of :class:`.CacheRegion`. + affect the data in the cache in any way, and is + **local to this instance of :class:`.CacheRegion`.** + + .. warning:: + + The :meth:`.CacheRegion.invalidate` method's default mode of + operation is to set a timestamp **local to this CacheRegion + in this Python process only**. It does not impact other Python + processes or regions as the timestamp is **only stored locally in + memory**. To implement invalidation where the + timestamp is stored in the cache or similar so that all Python + processes can be affected by an invalidation timestamp, implement a + custom :class:`.RegionInvalidationStrategy`. Once set, the invalidation time is honored by the :meth:`.CacheRegion.get_or_create`, :meth:`.CacheRegion.get_or_create_multi` and :meth:`.CacheRegion.get` methods. - The method - supports both "hard" and "soft" invalidation options. With "hard" - invalidation, :meth:`.CacheRegion.get_or_create` will force an immediate - regeneration of the value which all getters will wait for. With - "soft" invalidation, subsequent getters will return the "old" value until - the new one is available. + The method supports both "hard" and "soft" invalidation + options. With "hard" invalidation, + :meth:`.CacheRegion.get_or_create` will force an immediate + regeneration of the value which all getters will wait for. + With "soft" invalidation, subsequent getters will return the + "old" value until the new one is available. Usage of "soft" invalidation requires that the region or the method is given a non-None expiration time. @@ -329,12 +553,7 @@ class CacheRegion(object): .. versionadded:: 0.5.1 """ - if hard: - self._hard_invalidated = time.time() - self._soft_invalidated = None - else: - self._hard_invalidated = None - self._soft_invalidated = time.time() + self.region_invalidator.invalidate(hard) def configure_from_config(self, config_dict, prefix): """Configure from a configuration dictionary @@ -363,13 +582,16 @@ class CacheRegion(object): config_dict = coerce_string_conf(config_dict) return self.configure( config_dict["%sbackend" % prefix], - expiration_time = config_dict.get( - "%sexpiration_time" % prefix, None), + expiration_time=config_dict.get( + "%sexpiration_time" % prefix, None), _config_argument_dict=config_dict, - _config_prefix="%sarguments." % prefix + _config_prefix="%sarguments." % prefix, + wrap=config_dict.get( + "%swrap" % prefix, None), + replace_existing_backend=config_dict.get( + "%sreplace_existing_backend" % prefix, False), ) - @memoized_property def backend(self): raise exception.RegionNotConfigured( @@ -443,7 +665,7 @@ class CacheRegion(object): key = self.key_mangler(key) value = self.backend.get(key) value = self._unexpired_value_fn( - expiration_time, ignore_expiration)(value) + expiration_time, ignore_expiration)(value) return value.payload @@ -456,15 +678,14 @@ class CacheRegion(object): current_time = time.time() - invalidated = self._hard_invalidated or self._soft_invalidated def value_fn(value): if value is NO_VALUE: return value elif expiration_time is not None and \ - current_time - value.metadata["ct"] > expiration_time: + current_time - value.metadata["ct"] > expiration_time: return NO_VALUE - elif invalidated and \ - value.metadata["ct"] < invalidated: + elif self.region_invalidator.is_invalidated( + value.metadata["ct"]): return NO_VALUE else: return value @@ -512,18 +733,19 @@ class CacheRegion(object): backend_values = self.backend.get_multi(keys) _unexpired_value_fn = self._unexpired_value_fn( - expiration_time, ignore_expiration) + expiration_time, ignore_expiration) return [ - value.payload if value is not NO_VALUE else value - for value in - ( - _unexpired_value_fn(value) for value in - backend_values - ) - ] + value.payload if value is not NO_VALUE else value + for value in + ( + _unexpired_value_fn(value) for value in + backend_values + ) + ] - def get_or_create(self, key, creator, expiration_time=None, - should_cache_fn=None): + def get_or_create( + self, key, creator, expiration_time=None, should_cache_fn=None, + creator_args=None): """Return a cached value based on the given key. If the value does not exist or is considered to be expired @@ -559,14 +781,19 @@ class CacheRegion(object): :param creator: function which creates a new value. + :param creator_args: optional tuple of (args, kwargs) that will be + passed to the creator function if present. + + .. versionadded:: 0.7.0 + :param expiration_time: optional expiration time which will overide the expiration time already configured on this :class:`.CacheRegion` if not None. To set no expiration, use the value -1. - :param should_cache_fn: optional callable function which will receive the - value returned by the "creator", and will then return True or False, - indicating if the value should actually be cached or not. If it - returns False, the value is still returned, but isn't cached. + :param should_cache_fn: optional callable function which will receive + the value returned by the "creator", and will then return True or + False, indicating if the value should actually be cached or not. If + it returns False, the value is still returned, but isn't cached. E.g.:: def dont_cache_none(value): @@ -587,7 +814,8 @@ class CacheRegion(object): :meth:`.CacheRegion.cache_on_arguments` - applies :meth:`.get_or_create` to any function using a decorator. - :meth:`.CacheRegion.get_or_create_multi` - multiple key/value version + :meth:`.CacheRegion.get_or_create_multi` - multiple key/value + version """ orig_key = key @@ -596,20 +824,21 @@ class CacheRegion(object): def get_value(): value = self.backend.get(key) - if value is NO_VALUE or \ - value.metadata['v'] != value_version or \ - (self._hard_invalidated and - value.metadata["ct"] < self._hard_invalidated): + if (value is NO_VALUE or value.metadata['v'] != value_version or + self.region_invalidator.is_hard_invalidated( + value.metadata["ct"])): raise NeedRegenerationException() ct = value.metadata["ct"] - if self._soft_invalidated: - if ct < self._soft_invalidated: - ct = time.time() - expiration_time - .0001 + if self.region_invalidator.is_soft_invalidated(ct): + ct = time.time() - expiration_time - .0001 return value.payload, ct def gen_value(): - created_value = creator() + if creator_args: + created_value = creator(*creator_args[0], **creator_args[1]) + else: + created_value = creator() value = self._value(created_value) if not should_cache_fn or \ @@ -621,14 +850,24 @@ class CacheRegion(object): if expiration_time is None: expiration_time = self.expiration_time - if expiration_time is None and self._soft_invalidated: + if (expiration_time is None and + self.region_invalidator.was_soft_invalidated()): raise exception.DogpileCacheException( - "Non-None expiration time required " - "for soft invalidation") + "Non-None expiration time required " + "for soft invalidation") + + if expiration_time == -1: + expiration_time = None if self.async_creation_runner: def async_creator(mutex): - return self.async_creation_runner(self, orig_key, creator, mutex) + if creator_args: + @wraps(creator) + def go(): + return creator(*creator_args[0], **creator_args[1]) + else: + go = creator + return self.async_creation_runner(self, orig_key, go, mutex) else: async_creator = None @@ -640,8 +879,8 @@ class CacheRegion(object): async_creator) as value: return value - def get_or_create_multi(self, keys, creator, expiration_time=None, - should_cache_fn=None): + def get_or_create_multi( + self, keys, creator, expiration_time=None, should_cache_fn=None): """Return a sequence of cached values based on a sequence of keys. The behavior for generation of values based on keys corresponds @@ -655,6 +894,13 @@ class CacheRegion(object): and :meth:`.Region.set_multi` to get and set values from the backend. + If you are using a :class:`.CacheBackend` or :class:`.ProxyBackend` + that modifies values, take note this function invokes + ``.set_multi()`` for newly generated values using the same values it + returns to the calling function. A correct implementation of + ``.set_multi()`` will not modify values in-place on the submitted + ``mapping`` dict. + :param keys: Sequence of keys to be retrieved. :param creator: function which accepts a sequence of keys and @@ -665,9 +911,9 @@ class CacheRegion(object): if not None. To set no expiration, use the value -1. :param should_cache_fn: optional callable function which will receive - each value returned by the "creator", and will then return True or False, - indicating if the value should actually be cached or not. If it - returns False, the value is still returned, but isn't cached. + each value returned by the "creator", and will then return True or + False, indicating if the value should actually be cached or not. If + it returns False, the value is still returned, but isn't cached. .. versionadded:: 0.5.0 @@ -683,19 +929,17 @@ class CacheRegion(object): def get_value(key): value = values.get(key, NO_VALUE) - if value is NO_VALUE or \ - value.metadata['v'] != value_version or \ - (self._hard_invalidated and - value.metadata["ct"] < self._hard_invalidated): + if (value is NO_VALUE or value.metadata['v'] != value_version or + self.region_invalidator.is_hard_invalidated( + value.metadata['ct'])): # dogpile.core understands a 0 here as # "the value is not available", e.g. # _has_value() will return False. return value.payload, 0 else: ct = value.metadata["ct"] - if self._soft_invalidated: - if ct < self._soft_invalidated: - ct = time.time() - expiration_time - .0001 + if self.region_invalidator.is_soft_invalidated(ct): + ct = time.time() - expiration_time - .0001 return value.payload, ct @@ -708,10 +952,14 @@ class CacheRegion(object): if expiration_time is None: expiration_time = self.expiration_time - if expiration_time is None and self._soft_invalidated: + if (expiration_time is None and + self.region_invalidator.was_soft_invalidated()): raise exception.DogpileCacheException( - "Non-None expiration time required " - "for soft invalidation") + "Non-None expiration time required " + "for soft invalidation") + + if expiration_time == -1: + expiration_time = None mutexes = {} @@ -732,8 +980,8 @@ class CacheRegion(object): gen_value, lambda: get_value(mangled_key), expiration_time, - async_creator=lambda mutex: - async_creator(orig_key, mutex)): + async_creator=lambda mutex: async_creator(orig_key, mutex) + ): pass try: if mutexes: @@ -750,11 +998,14 @@ class CacheRegion(object): if not should_cache_fn: self.backend.set_multi(values_w_created) else: - self.backend.set_multi(dict( + values_to_cache = dict( (k, v) for k, v in values_w_created.items() if should_cache_fn(v[0]) - )) + ) + + if values_to_cache: + self.backend.set_multi(values_to_cache) values.update(values_w_created) return [values[orig_to_mangled[k]].payload for k in keys] @@ -762,13 +1013,14 @@ class CacheRegion(object): for mutex in mutexes.values(): mutex.release() - def _value(self, value): """Return a :class:`.CachedValue` given a value.""" - return CachedValue(value, { - "ct": time.time(), - "v": value_version - }) + return CachedValue( + value, + { + "ct": time.time(), + "v": value_version + }) def set(self, key, value): """Place a new value in the cache under the given key.""" @@ -777,7 +1029,6 @@ class CacheRegion(object): key = self.key_mangler(key) self.backend.set(key, self._value(value)) - def set_multi(self, mapping): """Place new values in the cache under the given keys. @@ -788,13 +1039,13 @@ class CacheRegion(object): return if self.key_mangler: - mapping = dict((self.key_mangler(k), self._value(v)) - for k, v in mapping.items()) + mapping = dict(( + self.key_mangler(k), self._value(v)) + for k, v in mapping.items()) else: mapping = dict((k, self._value(v)) for k, v in mapping.items()) self.backend.set_multi(mapping) - def delete(self, key): """Remove a value from the cache. @@ -807,7 +1058,6 @@ class CacheRegion(object): self.backend.delete(key) - def delete_multi(self, keys): """Remove multiple values from the cache. @@ -823,11 +1073,12 @@ class CacheRegion(object): self.backend.delete_multi(keys) - - def cache_on_arguments(self, namespace=None, - expiration_time=None, - should_cache_fn=None, - to_str=compat.string_type): + def cache_on_arguments( + self, namespace=None, + expiration_time=None, + should_cache_fn=None, + to_str=compat.string_type, + function_key_generator=None): """A function decorator that will cache the return value of the function using a key derived from the function itself and its arguments. @@ -866,9 +1117,9 @@ class CacheRegion(object): generate_something.set(3, 5, 6) - The above example is equivalent to calling ``generate_something(5, 6)``, - if the function were to produce the value ``3`` as the value to be - cached. + The above example is equivalent to calling + ``generate_something(5, 6)``, if the function were to produce + the value ``3`` as the value to be cached. .. versionadded:: 0.4.1 Added ``set()`` method to decorated function. @@ -881,6 +1132,14 @@ class CacheRegion(object): .. versionadded:: 0.5.0 Added ``refresh()`` method to decorated function. + ``original()`` on other hand will invoke the decorated function + without any caching:: + + newvalue = generate_something.original(5, 6) + + .. versionadded:: 0.6.0 Added ``original()`` method to decorated + function. + Lastly, the ``get()`` method returns either the value cached for the given key, or the token ``NO_VALUE`` if no such key exists:: @@ -986,6 +1245,12 @@ class CacheRegion(object): .. versionadded:: 0.5.0 + :param function_key_generator: a function that will produce a + "cache key". This function will supersede the one configured on the + :class:`.CacheRegion` itself. + + .. versionadded:: 0.5.5 + .. seealso:: :meth:`.CacheRegion.cache_multi_on_arguments` @@ -994,23 +1259,35 @@ class CacheRegion(object): """ expiration_time_is_callable = compat.callable(expiration_time) - def decorator(fn): + + if function_key_generator is None: + function_key_generator = self.function_key_generator + + def get_or_create_for_user_func(key_generator, user_func, *arg, **kw): + key = key_generator(*arg, **kw) + + timeout = expiration_time() if expiration_time_is_callable \ + else expiration_time + return self.get_or_create(key, user_func, timeout, + should_cache_fn, (arg, kw)) + + def cache_decorator(user_func): if to_str is compat.string_type: # backwards compatible - key_generator = self.function_key_generator(namespace, fn) + key_generator = function_key_generator(namespace, user_func) else: - key_generator = self.function_key_generator(namespace, fn, - to_str=to_str) - @wraps(fn) - def decorate(*arg, **kw): + key_generator = function_key_generator( + namespace, user_func, + to_str=to_str) + + def refresh(*arg, **kw): + """ + Like invalidate, but regenerates the value instead + """ key = key_generator(*arg, **kw) - @wraps(fn) - def creator(): - return fn(*arg, **kw) - timeout = expiration_time() if expiration_time_is_callable \ - else expiration_time - return self.get_or_create(key, creator, timeout, - should_cache_fn) + value = user_func(*arg, **kw) + self.set(key, value) + return value def invalidate(*arg, **kw): key = key_generator(*arg, **kw) @@ -1024,23 +1301,24 @@ class CacheRegion(object): key = key_generator(*arg, **kw) return self.get(key) - def refresh(*arg, **kw): - key = key_generator(*arg, **kw) - value = fn(*arg, **kw) - self.set(key, value) - return value + user_func.set = set_ + user_func.invalidate = invalidate + user_func.get = get + user_func.refresh = refresh + user_func.original = user_func - decorate.set = set_ - decorate.invalidate = invalidate - decorate.refresh = refresh - decorate.get = get + # Use `decorate` to preserve the signature of :param:`user_func`. - return decorate - return decorator + return decorate(user_func, partial( + get_or_create_for_user_func, key_generator)) - def cache_multi_on_arguments(self, namespace=None, expiration_time=None, - should_cache_fn=None, - asdict=False, to_str=compat.string_type): + return cache_decorator + + def cache_multi_on_arguments( + self, namespace=None, expiration_time=None, + should_cache_fn=None, + asdict=False, to_str=compat.string_type, + function_multi_key_generator=None): """A function decorator that will cache multiple return values from the function using a sequence of keys derived from the function itself and the arguments passed to it. @@ -1058,15 +1336,16 @@ class CacheRegion(object): ] The decorated function can be called normally. The decorator - will produce a list of cache keys using a mechanism similar to that - of :meth:`.CacheRegion.cache_on_arguments`, combining the name - of the function with the optional namespace and with the string form - of each key. It will then consult the cache using the same mechanism as that - of :meth:`.CacheRegion.get_multi` to retrieve all current values; - the originally passed keys corresponding to those values which - aren't generated or need regeneration will - be assembled into a new argument list, and the decorated function - is then called with that subset of arguments. + will produce a list of cache keys using a mechanism similar to + that of :meth:`.CacheRegion.cache_on_arguments`, combining the + name of the function with the optional namespace and with the + string form of each key. It will then consult the cache using + the same mechanism as that of :meth:`.CacheRegion.get_multi` + to retrieve all current values; the originally passed keys + corresponding to those values which aren't generated or need + regeneration will be assembled into a new argument list, and + the decorated function is then called with that subset of + arguments. The returned result is a list:: @@ -1077,10 +1356,10 @@ class CacheRegion(object): cache and conditionally call the function. See that method for additional behavioral details. - Unlike the :meth:`.CacheRegion.cache_on_arguments` - method, :meth:`.CacheRegion.cache_multi_on_arguments` works only - with a single function signature, one which takes a simple list of keys as - arguments. + Unlike the :meth:`.CacheRegion.cache_on_arguments` method, + :meth:`.CacheRegion.cache_multi_on_arguments` works only with + a single function signature, one which takes a simple list of + keys as arguments. Like :meth:`.CacheRegion.cache_on_arguments`, the decorated function is also provided with a ``set()`` method, which here accepts a @@ -1119,9 +1398,10 @@ class CacheRegion(object): expiration time. May be passed as an integer or a callable. - :param should_cache_fn: passed to :meth:`.CacheRegion.get_or_create_multi`. - This function is given a value as returned by the creator, and only - if it returns True will that value be placed in the cache. + :param should_cache_fn: passed to + :meth:`.CacheRegion.get_or_create_multi`. This function is given a + value as returned by the creator, and only if it returns True will + that value be placed in the cache. :param asdict: if ``True``, the decorated function should return its result as a dictionary of keys->values, and the final result @@ -1142,6 +1422,12 @@ class CacheRegion(object): .. versionadded:: 0.5.0 + :param function_multi_key_generator: a function that will produce a + list of keys. This function will supersede the one configured on the + :class:`.CacheRegion` itself. + + .. versionadded:: 0.5.5 + .. seealso:: :meth:`.CacheRegion.cache_on_arguments` @@ -1150,43 +1436,53 @@ class CacheRegion(object): """ expiration_time_is_callable = compat.callable(expiration_time) - def decorator(fn): - key_generator = self.function_multi_key_generator(namespace, fn, - to_str=to_str) - @wraps(fn) - def decorate(*arg, **kw): - cache_keys = arg - keys = key_generator(*arg, **kw) - key_lookup = dict(zip(keys, cache_keys)) - @wraps(fn) - def creator(*keys_to_create): - return fn(*[key_lookup[k] for k in keys_to_create]) - timeout = expiration_time() if expiration_time_is_callable \ - else expiration_time + if function_multi_key_generator is None: + function_multi_key_generator = self.function_multi_key_generator - if asdict: - def dict_create(*keys): - d_values = creator(*keys) - return [d_values.get(key_lookup[k], NO_VALUE) for k in keys] + def get_or_create_for_user_func(key_generator, user_func, *arg, **kw): + cache_keys = arg + keys = key_generator(*arg, **kw) + key_lookup = dict(zip(keys, cache_keys)) - def wrap_cache_fn(value): - if value is NO_VALUE: - return False - elif not should_cache_fn: - return True - else: - return should_cache_fn(value) + @wraps(user_func) + def creator(*keys_to_create): + return user_func(*[key_lookup[k] for k in keys_to_create]) - result = self.get_or_create_multi(keys, dict_create, timeout, - wrap_cache_fn) - result = dict((k, v) for k, v in zip(cache_keys, result) - if v is not NO_VALUE) - else: - result = self.get_or_create_multi(keys, creator, timeout, - should_cache_fn) + timeout = expiration_time() if expiration_time_is_callable \ + else expiration_time - return result + if asdict: + def dict_create(*keys): + d_values = creator(*keys) + return [ + d_values.get(key_lookup[k], NO_VALUE) + for k in keys] + + def wrap_cache_fn(value): + if value is NO_VALUE: + return False + elif not should_cache_fn: + return True + else: + return should_cache_fn(value) + + result = self.get_or_create_multi( + keys, dict_create, timeout, wrap_cache_fn) + result = dict( + (k, v) for k, v in zip(cache_keys, result) + if v is not NO_VALUE) + else: + result = self.get_or_create_multi( + keys, creator, timeout, + should_cache_fn) + + return result + + def cache_decorator(user_func): + key_generator = function_multi_key_generator( + namespace, user_func, + to_str=to_str) def invalidate(*arg): keys = key_generator(*arg) @@ -1196,10 +1492,10 @@ class CacheRegion(object): keys = list(mapping) gen_keys = key_generator(*keys) self.set_multi(dict( - (gen_key, mapping[key]) - for gen_key, key - in zip(gen_keys, keys)) - ) + (gen_key, mapping[key]) + for gen_key, key + in zip(gen_keys, keys)) + ) def get(*arg): keys = key_generator(*arg) @@ -1207,25 +1503,29 @@ class CacheRegion(object): def refresh(*arg): keys = key_generator(*arg) - values = fn(*arg) + values = user_func(*arg) if asdict: self.set_multi( - dict(zip(keys, [values[a] for a in arg])) - ) + dict(zip(keys, [values[a] for a in arg])) + ) return values else: self.set_multi( - dict(zip(keys, values)) - ) + dict(zip(keys, values)) + ) return values - decorate.set = set_ - decorate.invalidate = invalidate - decorate.refresh = refresh - decorate.get = get + user_func.set = set_ + user_func.invalidate = invalidate + user_func.refresh = refresh + user_func.get = get + + # Use `decorate` to preserve the signature of :param:`user_func`. + + return decorate(user_func, partial(get_or_create_for_user_func, key_generator)) + + return cache_decorator - return decorate - return decorator diff --git a/libs/dogpile/cache/util.py b/libs/dogpile/cache/util.py index 0f732a6c..16bcd1c9 100644 --- a/libs/dogpile/cache/util.py +++ b/libs/dogpile/cache/util.py @@ -1,57 +1,6 @@ from hashlib import sha1 -import inspect -import re -import collections -from . import compat - - -def coerce_string_conf(d): - result = {} - for k, v in d.items(): - if not isinstance(v, compat.string_types): - result[k] = v - continue - - v = v.strip() - if re.match(r'^[-+]?\d+$', v): - result[k] = int(v) - elif re.match(r'^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?$', v): - result[k] = float(v) - elif v.lower() in ('false', 'true'): - result[k] = v.lower() == 'true' - elif v == 'None': - result[k] = None - else: - result[k] = v - return result - -class PluginLoader(object): - def __init__(self, group): - self.group = group - self.impls = {} - - def load(self, name): - if name in self.impls: - return self.impls[name]() - else: # pragma NO COVERAGE - import pkg_resources - for impl in pkg_resources.iter_entry_points( - self.group, - name): - self.impls[name] = impl.load - return impl.load() - else: - raise Exception( - "Can't load plugin %s %s" % - (self.group, name)) - - def register(self, name, modulepath, objname): - def load(): - mod = __import__(modulepath) - for token in modulepath.split(".")[1:]: - mod = getattr(mod, token) - return getattr(mod, objname) - self.impls[name] = load +from ..util import compat +from ..util import langhelpers def function_key_generator(namespace, fn, to_str=compat.string_type): @@ -62,8 +11,14 @@ def function_key_generator(namespace, fn, to_str=compat.string_type): This is used by :meth:`.CacheRegion.cache_on_arguments` to generate a cache key from a decorated function. - It can be replaced using the ``function_key_generator`` - argument passed to :func:`.make_region`. + An alternate function may be used by specifying + the :paramref:`.CacheRegion.function_key_generator` argument + for :class:`.CacheRegion`. + + .. seealso:: + + :func:`.kwarg_function_key_generator` - similar function that also + takes keyword arguments into account """ @@ -72,19 +27,21 @@ def function_key_generator(namespace, fn, to_str=compat.string_type): else: namespace = '%s:%s|%s' % (fn.__module__, fn.__name__, namespace) - args = inspect.getargspec(fn) + args = compat.inspect_getargspec(fn) has_self = args[0] and args[0][0] in ('self', 'cls') + def generate_key(*args, **kw): if kw: raise ValueError( - "dogpile.cache's default key creation " - "function does not accept keyword arguments.") + "dogpile.cache's default key creation " + "function does not accept keyword arguments.") if has_self: args = args[1:] return namespace + "|" + " ".join(map(to_str, args)) return generate_key + def function_multi_key_generator(namespace, fn, to_str=compat.string_type): if namespace is None: @@ -92,23 +49,80 @@ def function_multi_key_generator(namespace, fn, to_str=compat.string_type): else: namespace = '%s:%s|%s' % (fn.__module__, fn.__name__, namespace) - args = inspect.getargspec(fn) + args = compat.inspect_getargspec(fn) has_self = args[0] and args[0][0] in ('self', 'cls') + def generate_keys(*args, **kw): if kw: raise ValueError( - "dogpile.cache's default key creation " - "function does not accept keyword arguments.") + "dogpile.cache's default key creation " + "function does not accept keyword arguments.") if has_self: args = args[1:] return [namespace + "|" + key for key in map(to_str, args)] return generate_keys + +def kwarg_function_key_generator(namespace, fn, to_str=compat.string_type): + """Return a function that generates a string + key, based on a given function as well as + arguments to the returned function itself. + + For kwargs passed in, we will build a dict of + all argname (key) argvalue (values) including + default args from the argspec and then + alphabetize the list before generating the + key. + + .. versionadded:: 0.6.2 + + .. seealso:: + + :func:`.function_key_generator` - default key generation function + + """ + + if namespace is None: + namespace = '%s:%s' % (fn.__module__, fn.__name__) + else: + namespace = '%s:%s|%s' % (fn.__module__, fn.__name__, namespace) + + argspec = compat.inspect_getargspec(fn) + default_list = list(argspec.defaults or []) + # Reverse the list, as we want to compare the argspec by negative index, + # meaning default_list[0] should be args[-1], which works well with + # enumerate() + default_list.reverse() + # use idx*-1 to create the correct right-lookup index. + args_with_defaults = dict((argspec.args[(idx*-1)], default) + for idx, default in enumerate(default_list, 1)) + if argspec.args and argspec.args[0] in ('self', 'cls'): + arg_index_start = 1 + else: + arg_index_start = 0 + + def generate_key(*args, **kwargs): + as_kwargs = dict( + [(argspec.args[idx], arg) + for idx, arg in enumerate(args[arg_index_start:], + arg_index_start)]) + as_kwargs.update(kwargs) + for arg, val in args_with_defaults.items(): + if arg not in as_kwargs: + as_kwargs[arg] = val + + argument_values = [as_kwargs[key] + for key in sorted(as_kwargs.keys())] + return namespace + '|' + " ".join(map(to_str, argument_values)) + return generate_key + + def sha1_mangle_key(key): """a SHA1 key mangler.""" return sha1(key).hexdigest() + def length_conditional_mangler(length, mangler): """a key mangler that mangles if the length of the key is past a certain threshold. @@ -121,69 +135,11 @@ def length_conditional_mangler(length, mangler): return key return mangle -class memoized_property(object): - """A read-only @property that is only evaluated once.""" - def __init__(self, fget, doc=None): - self.fget = fget - self.__doc__ = doc or fget.__doc__ - self.__name__ = fget.__name__ +# in the 0.6 release these functions were moved to the dogpile.util namespace. +# They are linked here to maintain compatibility with older versions. - def __get__(self, obj, cls): - if obj is None: - return self - obj.__dict__[self.__name__] = result = self.fget(obj) - return result - -def to_list(x, default=None): - """Coerce to a list.""" - if x is None: - return default - if not isinstance(x, (list, tuple)): - return [x] - else: - return x - - -class KeyReentrantMutex(object): - - def __init__(self, key, mutex, keys): - self.key = key - self.mutex = mutex - self.keys = keys - - @classmethod - def factory(cls, mutex): - # this collection holds zero or one - # thread idents as the key; a set of - # keynames held as the value. - keystore = collections.defaultdict(set) - def fac(key): - return KeyReentrantMutex(key, mutex, keystore) - return fac - - def acquire(self, wait=True): - current_thread = compat.threading.current_thread().ident - keys = self.keys.get(current_thread) - if keys is not None and \ - self.key not in keys: - # current lockholder, new key. add it in - keys.add(self.key) - return True - elif self.mutex.acquire(wait=wait): - # after acquire, create new set and add our key - self.keys[current_thread].add(self.key) - return True - else: - return False - - def release(self): - current_thread = compat.threading.current_thread().ident - keys = self.keys.get(current_thread) - assert keys is not None, "this thread didn't do the acquire" - assert self.key in keys, "No acquire held for key '%s'" % self.key - keys.remove(self.key) - if not keys: - # when list of keys empty, remove - # the thread ident and unlock. - del self.keys[current_thread] - self.mutex.release() +coerce_string_conf = langhelpers.coerce_string_conf +KeyReentrantMutex = langhelpers.KeyReentrantMutex +memoized_property = langhelpers.memoized_property +PluginLoader = langhelpers.PluginLoader +to_list = langhelpers.to_list diff --git a/libs/dogpile/core.py b/libs/dogpile/core.py new file mode 100644 index 00000000..2bcfaf81 --- /dev/null +++ b/libs/dogpile/core.py @@ -0,0 +1,17 @@ +"""Compatibility namespace for those using dogpile.core. + +As of dogpile.cache 0.6.0, dogpile.core as a separate package +is no longer used by dogpile.cache. + +Note that this namespace will not take effect if an actual +dogpile.core installation is present. + +""" + +from .util import nameregistry # noqa +from .util import readwrite_lock # noqa +from .util.readwrite_lock import ReadWriteMutex # noqa +from .util.nameregistry import NameRegistry # noqa +from .lock import Lock # noqa +from .lock import NeedRegenerationException # noqa +from . import __version__ # noqa diff --git a/libs/dogpile/core/__init__.py b/libs/dogpile/core/__init__.py deleted file mode 100644 index fb9d756d..00000000 --- a/libs/dogpile/core/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from .dogpile import NeedRegenerationException, Lock -from .nameregistry import NameRegistry -from .readwrite_lock import ReadWriteMutex -from .legacy import Dogpile, SyncReaderDogpile - -__all__ = [ - 'Dogpile', 'SyncReaderDogpile', 'NeedRegenerationException', - 'NameRegistry', 'ReadWriteMutex', 'Lock'] - -__version__ = '0.4.1' - diff --git a/libs/dogpile/core/legacy.py b/libs/dogpile/core/legacy.py deleted file mode 100644 index dad4e160..00000000 --- a/libs/dogpile/core/legacy.py +++ /dev/null @@ -1,154 +0,0 @@ -from __future__ import with_statement - -from .util import threading -from .readwrite_lock import ReadWriteMutex -from .dogpile import Lock -import time -import contextlib - -class Dogpile(object): - """Dogpile lock class. - - .. deprecated:: 0.4.0 - The :class:`.Lock` object specifies the full - API of the :class:`.Dogpile` object in a single way, - rather than providing multiple modes of usage which - don't necessarily work in the majority of cases. - :class:`.Dogpile` is now a wrapper around the :class:`.Lock` object - which provides dogpile.core's original usage pattern. - This usage pattern began as something simple, but was - not of general use in real-world caching environments without - several extra complicating factors; the :class:`.Lock` - object presents the "real-world" API more succinctly, - and also fixes a cross-process concurrency issue. - - :param expiretime: Expiration time in seconds. Set to - ``None`` for never expires. - :param init: if True, set the 'createdtime' to the - current time. - :param lock: a mutex object that provides - ``acquire()`` and ``release()`` methods. - - """ - def __init__(self, expiretime, init=False, lock=None): - """Construct a new :class:`.Dogpile`. - - """ - if lock: - self.dogpilelock = lock - else: - self.dogpilelock = threading.Lock() - - self.expiretime = expiretime - if init: - self.createdtime = time.time() - - createdtime = -1 - """The last known 'creation time' of the value, - stored as an epoch (i.e. from ``time.time()``). - - If the value here is -1, it is assumed the value - should recreate immediately. - - """ - - def acquire(self, creator, - value_fn=None, - value_and_created_fn=None): - """Acquire the lock, returning a context manager. - - :param creator: Creation function, used if this thread - is chosen to create a new value. - - :param value_fn: Optional function that returns - the value from some datasource. Will be returned - if regeneration is not needed. - - :param value_and_created_fn: Like value_fn, but returns a tuple - of (value, createdtime). The returned createdtime - will replace the "createdtime" value on this dogpile - lock. This option removes the need for the dogpile lock - itself to remain persistent across usages; another - dogpile can come along later and pick up where the - previous one left off. - - """ - - if value_and_created_fn is None: - if value_fn is None: - def value_and_created_fn(): - return None, self.createdtime - else: - def value_and_created_fn(): - return value_fn(), self.createdtime - - def creator_wrapper(): - value = creator() - self.createdtime = time.time() - return value, self.createdtime - else: - def creator_wrapper(): - value = creator() - self.createdtime = time.time() - return value - - return Lock( - self.dogpilelock, - creator_wrapper, - value_and_created_fn, - self.expiretime - ) - - @property - def is_expired(self): - """Return true if the expiration time is reached, or no - value is available.""" - - return not self.has_value or \ - ( - self.expiretime is not None and - time.time() - self.createdtime > self.expiretime - ) - - @property - def has_value(self): - """Return true if the creation function has proceeded - at least once.""" - return self.createdtime > 0 - - -class SyncReaderDogpile(Dogpile): - """Provide a read-write lock function on top of the :class:`.Dogpile` - class. - - .. deprecated:: 0.4.0 - The :class:`.ReadWriteMutex` object can be used directly. - - """ - def __init__(self, *args, **kw): - super(SyncReaderDogpile, self).__init__(*args, **kw) - self.readwritelock = ReadWriteMutex() - - @contextlib.contextmanager - def acquire_write_lock(self): - """Return the "write" lock context manager. - - This will provide a section that is mutexed against - all readers/writers for the dogpile-maintained value. - - """ - - self.readwritelock.acquire_write_lock() - try: - yield - finally: - self.readwritelock.release_write_lock() - - @contextlib.contextmanager - def acquire(self, *arg, **kw): - with super(SyncReaderDogpile, self).acquire(*arg, **kw) as value: - self.readwritelock.acquire_read_lock() - try: - yield value - finally: - self.readwritelock.release_read_lock() diff --git a/libs/dogpile/core/util.py b/libs/dogpile/core/util.py deleted file mode 100644 index f53c6818..00000000 --- a/libs/dogpile/core/util.py +++ /dev/null @@ -1,8 +0,0 @@ -import sys -py3k = sys.version_info >= (3, 0) - -try: - import threading -except ImportError: - import dummy_threading as threading - diff --git a/libs/dogpile/core/dogpile.py b/libs/dogpile/lock.py similarity index 54% rename from libs/dogpile/core/dogpile.py rename to libs/dogpile/lock.py index 2e3ca0e9..2ac22dcf 100644 --- a/libs/dogpile/core/dogpile.py +++ b/libs/dogpile/lock.py @@ -3,6 +3,7 @@ import logging log = logging.getLogger(__name__) + class NeedRegenerationException(Exception): """An exception that when raised in the 'with' block, forces the 'has_value' flag to False and incurs a @@ -12,6 +13,7 @@ class NeedRegenerationException(Exception): NOT_REGENERATED = object() + class Lock(object): """Dogpile lock class. @@ -21,11 +23,6 @@ class Lock(object): continue to return the previous version of that value. - .. versionadded:: 0.4.0 - The :class:`.Lock` class was added as a single-use object - representing the dogpile API without dependence on - any shared state between multiple instances. - :param mutex: A mutex object that provides ``acquire()`` and ``release()`` methods. :param creator: Callable which returns a tuple of the form @@ -52,17 +49,16 @@ class Lock(object): this to be used to defer invocation of the creator callable until some later time. - .. versionadded:: 0.4.1 added the async_creator argument. - """ - def __init__(self, - mutex, - creator, - value_and_created_fn, - expiretime, - async_creator=None, - ): + def __init__( + self, + mutex, + creator, + value_and_created_fn, + expiretime, + async_creator=None, + ): self.mutex = mutex self.creator = creator self.value_and_created_fn = value_and_created_fn @@ -73,11 +69,10 @@ class Lock(object): """Return true if the expiration time is reached, or no value is available.""" - return not self._has_value(createdtime) or \ - ( - self.expiretime is not None and - time.time() - createdtime > self.expiretime - ) + return not self._has_value(createdtime) or ( + self.expiretime is not None and + time.time() - createdtime > self.expiretime + ) def _has_value(self, createdtime): """Return true if the creation function has proceeded @@ -95,68 +90,100 @@ class Lock(object): value = NOT_REGENERATED createdtime = -1 - generated = self._enter_create(createdtime) + generated = self._enter_create(value, createdtime) if generated is not NOT_REGENERATED: generated, createdtime = generated return generated elif value is NOT_REGENERATED: + # we called upon the creator, and it said that it + # didn't regenerate. this typically means another + # thread is running the creation function, and that the + # cache should still have a value. However, + # we don't have a value at all, which is unusual since we just + # checked for it, so check again (TODO: is this a real codepath?) try: value, createdtime = value_fn() return value except NeedRegenerationException: - raise Exception("Generation function should " - "have just been called by a concurrent " - "thread.") + raise Exception( + "Generation function should " + "have just been called by a concurrent " + "thread.") else: return value - def _enter_create(self, createdtime): - + def _enter_create(self, value, createdtime): if not self._is_expired(createdtime): return NOT_REGENERATED - async = False + _async = False if self._has_value(createdtime): + has_value = True if not self.mutex.acquire(False): - log.debug("creation function in progress " - "elsewhere, returning") + log.debug( + "creation function in progress " + "elsewhere, returning") return NOT_REGENERATED else: + has_value = False log.debug("no value, waiting for create lock") self.mutex.acquire() try: log.debug("value creation lock %r acquired" % self.mutex) - # see if someone created the value already - try: - value, createdtime = self.value_and_created_fn() - except NeedRegenerationException: - pass - else: - if not self._is_expired(createdtime): - log.debug("value already present") - return value, createdtime - elif self.async_creator: - log.debug("Passing creation lock to async runner") - self.async_creator(self.mutex) - async = True - return value, createdtime + if not has_value: + # we entered without a value, or at least with "creationtime == + # 0". Run the "getter" function again, to see if another + # thread has already generated the value while we waited on the + # mutex, or if the caller is otherwise telling us there is a + # value already which allows us to use async regeneration. (the + # latter is used by the multi-key routine). + try: + value, createdtime = self.value_and_created_fn() + except NeedRegenerationException: + # nope, nobody created the value, we're it. + # we must create it right now + pass + else: + has_value = True + # caller is telling us there is a value and that we can + # use async creation if it is expired. + if not self._is_expired(createdtime): + # it's not expired, return it + log.debug("Concurrent thread created the value") + return value, createdtime - log.debug("Calling creation function") - created = self.creator() - return created + # otherwise it's expired, call creator again + + if has_value and self.async_creator: + # we have a value we can return, safe to use async_creator + log.debug("Passing creation lock to async runner") + + # so...run it! + self.async_creator(self.mutex) + _async = True + + # and return the expired value for now + return value, createdtime + + # it's expired, and it's our turn to create it synchronously, *or*, + # there's no value at all, and we have to create it synchronously + log.debug( + "Calling creation function for %s value", + "not-yet-present" if not has_value else + "previously expired" + ) + return self.creator() finally: - if not async: + if not _async: self.mutex.release() log.debug("Released creation lock") - def __enter__(self): return self._enter() def __exit__(self, type, value, traceback): pass - diff --git a/libs/dogpile/util/__init__.py b/libs/dogpile/util/__init__.py new file mode 100644 index 00000000..91b07520 --- /dev/null +++ b/libs/dogpile/util/__init__.py @@ -0,0 +1,4 @@ +from .nameregistry import NameRegistry # noqa +from .readwrite_lock import ReadWriteMutex # noqa +from .langhelpers import PluginLoader, memoized_property, \ + coerce_string_conf, to_list, KeyReentrantMutex # noqa diff --git a/libs/dogpile/cache/compat.py b/libs/dogpile/util/compat.py similarity index 50% rename from libs/dogpile/cache/compat.py rename to libs/dogpile/util/compat.py index eb42d5f3..198c7627 100644 --- a/libs/dogpile/cache/compat.py +++ b/libs/dogpile/util/compat.py @@ -1,6 +1,5 @@ import sys - py2k = sys.version_info < (3, 0) py3k = sys.version_info >= (3, 0) py32 = sys.version_info >= (3, 2) @@ -11,10 +10,10 @@ win32 = sys.platform.startswith('win') try: import threading except ImportError: - import dummy_threading as threading + import dummy_threading as threading # noqa -if py3k: # pragma: no cover +if py3k: # pragma: no cover string_types = str, text_type = str string_type = str @@ -45,24 +44,44 @@ else: def ue(s): return unicode(s, "unicode_escape") - import ConfigParser as configparser - import StringIO as io + import ConfigParser as configparser # noqa + import StringIO as io # noqa + + callable = callable # noqa + import thread # noqa - callable = callable - import thread +if py3k: + import collections + ArgSpec = collections.namedtuple( + "ArgSpec", + ["args", "varargs", "keywords", "defaults"]) + from inspect import getfullargspec as inspect_getfullargspec + + def inspect_getargspec(func): + return ArgSpec( + *inspect_getfullargspec(func)[0:4] + ) +else: + from inspect import getargspec as inspect_getargspec # noqa if py3k or jython: import pickle else: - import cPickle as pickle + import cPickle as pickle # noqa + +if py3k: + def read_config_file(config, fileobj): + return config.read_file(fileobj) +else: + def read_config_file(config, fileobj): + return config.readfp(fileobj) def timedelta_total_seconds(td): if py27: return td.total_seconds() else: - return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 1e6) / 1e6 - - + return (td.microseconds + ( + td.seconds + td.days * 24 * 3600) * 1e6) / 1e6 diff --git a/libs/dogpile/util/langhelpers.py b/libs/dogpile/util/langhelpers.py new file mode 100644 index 00000000..4ff8e3e3 --- /dev/null +++ b/libs/dogpile/util/langhelpers.py @@ -0,0 +1,123 @@ +import re +import collections +from . import compat + + +def coerce_string_conf(d): + result = {} + for k, v in d.items(): + if not isinstance(v, compat.string_types): + result[k] = v + continue + + v = v.strip() + if re.match(r'^[-+]?\d+$', v): + result[k] = int(v) + elif re.match(r'^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?$', v): + result[k] = float(v) + elif v.lower() in ('false', 'true'): + result[k] = v.lower() == 'true' + elif v == 'None': + result[k] = None + else: + result[k] = v + return result + + +class PluginLoader(object): + def __init__(self, group): + self.group = group + self.impls = {} + + def load(self, name): + if name in self.impls: + return self.impls[name]() + else: # pragma NO COVERAGE + import pkg_resources + for impl in pkg_resources.iter_entry_points( + self.group, name): + self.impls[name] = impl.load + return impl.load() + else: + raise self.NotFound( + "Can't load plugin %s %s" % (self.group, name) + ) + + def register(self, name, modulepath, objname): + def load(): + mod = __import__(modulepath, fromlist=[objname]) + return getattr(mod, objname) + self.impls[name] = load + + class NotFound(Exception): + """The specified plugin could not be found.""" + + +class memoized_property(object): + """A read-only @property that is only evaluated once.""" + def __init__(self, fget, doc=None): + self.fget = fget + self.__doc__ = doc or fget.__doc__ + self.__name__ = fget.__name__ + + def __get__(self, obj, cls): + if obj is None: + return self + obj.__dict__[self.__name__] = result = self.fget(obj) + return result + + +def to_list(x, default=None): + """Coerce to a list.""" + if x is None: + return default + if not isinstance(x, (list, tuple)): + return [x] + else: + return x + + +class KeyReentrantMutex(object): + + def __init__(self, key, mutex, keys): + self.key = key + self.mutex = mutex + self.keys = keys + + @classmethod + def factory(cls, mutex): + # this collection holds zero or one + # thread idents as the key; a set of + # keynames held as the value. + keystore = collections.defaultdict(set) + + def fac(key): + return KeyReentrantMutex(key, mutex, keystore) + return fac + + def acquire(self, wait=True): + current_thread = compat.threading.current_thread().ident + keys = self.keys.get(current_thread) + if keys is not None and \ + self.key not in keys: + # current lockholder, new key. add it in + keys.add(self.key) + return True + elif self.mutex.acquire(wait=wait): + # after acquire, create new set and add our key + self.keys[current_thread].add(self.key) + return True + else: + return False + + def release(self): + current_thread = compat.threading.current_thread().ident + keys = self.keys.get(current_thread) + assert keys is not None, "this thread didn't do the acquire" + assert self.key in keys, "No acquire held for key '%s'" % self.key + keys.remove(self.key) + if not keys: + # when list of keys empty, remove + # the thread ident and unlock. + del self.keys[current_thread] + self.mutex.release() diff --git a/libs/dogpile/core/nameregistry.py b/libs/dogpile/util/nameregistry.py similarity index 93% rename from libs/dogpile/core/nameregistry.py rename to libs/dogpile/util/nameregistry.py index a73f450c..7087f7cd 100644 --- a/libs/dogpile/core/nameregistry.py +++ b/libs/dogpile/util/nameregistry.py @@ -1,6 +1,7 @@ -from .util import threading +from .compat import threading import weakref + class NameRegistry(object): """Generates and return an object, keeping it as a singleton for a certain identifier for as long as its @@ -49,7 +50,7 @@ class NameRegistry(object): self.creator = creator def get(self, identifier, *args, **kw): - """Get and possibly create the value. + r"""Get and possibly create the value. :param identifier: Hash key for the value. If the creation function is called, this identifier @@ -74,10 +75,12 @@ class NameRegistry(object): if identifier in self._values: return self._values[identifier] else: - self._values[identifier] = value = self.creator(identifier, *args, **kw) + self._values[identifier] = value = self.creator( + identifier, *args, **kw) return value except KeyError: - self._values[identifier] = value = self.creator(identifier, *args, **kw) + self._values[identifier] = value = self.creator( + identifier, *args, **kw) return value finally: self._mutex.release() diff --git a/libs/dogpile/core/readwrite_lock.py b/libs/dogpile/util/readwrite_lock.py similarity index 89% rename from libs/dogpile/core/readwrite_lock.py rename to libs/dogpile/util/readwrite_lock.py index 1ea25e47..9b953edb 100644 --- a/libs/dogpile/core/readwrite_lock.py +++ b/libs/dogpile/util/readwrite_lock.py @@ -1,27 +1,29 @@ -from .util import threading +from .compat import threading import logging log = logging.getLogger(__name__) + class LockError(Exception): pass + class ReadWriteMutex(object): """A mutex which allows multiple readers, single writer. - + :class:`.ReadWriteMutex` uses a Python ``threading.Condition`` to provide this functionality across threads within a process. - + The Beaker package also contained a file-lock based version of this concept, so that readers/writers could be synchronized - across processes with a common filesystem. A future Dogpile + across processes with a common filesystem. A future Dogpile release may include this additional class at some point. - + """ def __init__(self): # counts how many asynchronous methods are executing - self.async = 0 + self.async_ = 0 # pointer to thread that is the current sync operation self.current_sync_operation = None @@ -29,7 +31,7 @@ class ReadWriteMutex(object): # condition object to lock on self.condition = threading.Condition(threading.Lock()) - def acquire_read_lock(self, wait = True): + def acquire_read_lock(self, wait=True): """Acquire the 'read' lock.""" self.condition.acquire() try: @@ -43,35 +45,35 @@ class ReadWriteMutex(object): if self.current_sync_operation is not None: return False - self.async += 1 + self.async_ += 1 log.debug("%s acquired read lock", self) finally: self.condition.release() - if not wait: + if not wait: return True def release_read_lock(self): """Release the 'read' lock.""" self.condition.acquire() try: - self.async -= 1 + self.async_ -= 1 - # check if we are the last asynchronous reader thread + # check if we are the last asynchronous reader thread # out the door. - if self.async == 0: + if self.async_ == 0: # yes. so if a sync operation is waiting, notifyAll to wake # it up if self.current_sync_operation is not None: self.condition.notifyAll() - elif self.async < 0: + elif self.async_ < 0: raise LockError("Synchronizer error - too many " "release_read_locks called") log.debug("%s released read lock", self) finally: self.condition.release() - def acquire_write_lock(self, wait = True): + def acquire_write_lock(self, wait=True): """Acquire the 'write' lock.""" self.condition.acquire() try: @@ -88,13 +90,13 @@ class ReadWriteMutex(object): if self.current_sync_operation is not None: return False - # establish ourselves as the current sync + # establish ourselves as the current sync # this indicates to other read/write operations # that they should wait until this is None again self.current_sync_operation = threading.currentThread() # now wait again for asyncs to finish - if self.async > 0: + if self.async_ > 0: if wait: # wait self.condition.wait() @@ -106,7 +108,7 @@ class ReadWriteMutex(object): finally: self.condition.release() - if not wait: + if not wait: return True def release_write_lock(self): @@ -117,7 +119,7 @@ class ReadWriteMutex(object): raise LockError("Synchronizer error - current thread doesn't " "have the write lock") - # reset the current sync operation so + # reset the current sync operation so # another can get it self.current_sync_operation = None diff --git a/libs/enzyme/__init__.py b/libs/enzyme/__init__.py index 9a171b52..3bd89f33 100644 --- a/libs/enzyme/__init__.py +++ b/libs/enzyme/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- __title__ = 'enzyme' -__version__ = '0.4.2' +__version__ = '0.4.1' __author__ = 'Antoine Bertin' __license__ = 'Apache 2.0' __copyright__ = 'Copyright 2013 Antoine Bertin' diff --git a/libs/enzyme/tests/test_mkv.py b/libs/enzyme/tests/test_mkv.py index 2403661e..1bfa0456 100644 --- a/libs/enzyme/tests/test_mkv.py +++ b/libs/enzyme/tests/test_mkv.py @@ -21,8 +21,8 @@ def setUpModule(): class MKVTestCase(unittest.TestCase): def test_test1(self): - with io.open(os.path.join(TEST_DIR, 'test1.mkv'), 'rb') as stream: - mkv = MKV(stream) + stream = io.open(os.path.join(TEST_DIR, 'test1.mkv'), 'rb') + mkv = MKV(stream) # info self.assertTrue(mkv.info.title is None) self.assertTrue(mkv.info.duration == timedelta(minutes=1, seconds=27, milliseconds=336)) @@ -90,8 +90,8 @@ class MKVTestCase(unittest.TestCase): self.assertTrue(mkv.tags[0].simpletags[2].binary is None) def test_test2(self): - with io.open(os.path.join(TEST_DIR, 'test2.mkv'), 'rb') as stream: - mkv = MKV(stream) + stream = io.open(os.path.join(TEST_DIR, 'test2.mkv'), 'rb') + mkv = MKV(stream) # info self.assertTrue(mkv.info.title is None) self.assertTrue(mkv.info.duration == timedelta(seconds=47, milliseconds=509)) @@ -159,8 +159,8 @@ class MKVTestCase(unittest.TestCase): self.assertTrue(mkv.tags[0].simpletags[2].binary is None) def test_test3(self): - with io.open(os.path.join(TEST_DIR, 'test3.mkv'), 'rb') as stream: - mkv = MKV(stream) + stream = io.open(os.path.join(TEST_DIR, 'test3.mkv'), 'rb') + mkv = MKV(stream) # info self.assertTrue(mkv.info.title is None) self.assertTrue(mkv.info.duration == timedelta(seconds=49, milliseconds=64)) @@ -228,8 +228,8 @@ class MKVTestCase(unittest.TestCase): self.assertTrue(mkv.tags[0].simpletags[2].binary is None) def test_test5(self): - with io.open(os.path.join(TEST_DIR, 'test5.mkv'), 'rb') as stream: - mkv = MKV(stream) + stream = io.open(os.path.join(TEST_DIR, 'test5.mkv'), 'rb') + mkv = MKV(stream) # info self.assertTrue(mkv.info.title is None) self.assertTrue(mkv.info.duration == timedelta(seconds=46, milliseconds=665)) @@ -391,8 +391,8 @@ class MKVTestCase(unittest.TestCase): self.assertTrue(mkv.tags[0].simpletags[2].binary is None) def test_test6(self): - with io.open(os.path.join(TEST_DIR, 'test6.mkv'), 'rb') as stream: - mkv = MKV(stream) + stream = io.open(os.path.join(TEST_DIR, 'test6.mkv'), 'rb') + mkv = MKV(stream) # info self.assertTrue(mkv.info.title is None) self.assertTrue(mkv.info.duration == timedelta(seconds=87, milliseconds=336)) @@ -460,8 +460,8 @@ class MKVTestCase(unittest.TestCase): self.assertTrue(mkv.tags[0].simpletags[2].binary is None) def test_test7(self): - with io.open(os.path.join(TEST_DIR, 'test7.mkv'), 'rb') as stream: - mkv = MKV(stream) + stream = io.open(os.path.join(TEST_DIR, 'test7.mkv'), 'rb') + mkv = MKV(stream) # info self.assertTrue(mkv.info.title is None) self.assertTrue(mkv.info.duration == timedelta(seconds=37, milliseconds=43)) @@ -529,8 +529,8 @@ class MKVTestCase(unittest.TestCase): self.assertTrue(mkv.tags[0].simpletags[2].binary is None) def test_test8(self): - with io.open(os.path.join(TEST_DIR, 'test8.mkv'), 'rb') as stream: - mkv = MKV(stream) + stream = io.open(os.path.join(TEST_DIR, 'test8.mkv'), 'rb') + mkv = MKV(stream) # info self.assertTrue(mkv.info.title is None) self.assertTrue(mkv.info.duration == timedelta(seconds=47, milliseconds=341)) diff --git a/libs/pbr/__init__.py b/libs/pbr/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/pbr/builddoc.py b/libs/pbr/builddoc.py new file mode 100644 index 00000000..f5c66ce0 --- /dev/null +++ b/libs/pbr/builddoc.py @@ -0,0 +1,292 @@ +# Copyright 2011 OpenStack Foundation +# Copyright 2012-2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from distutils import log +import fnmatch +import os +import sys + +try: + import cStringIO +except ImportError: + import io as cStringIO + +try: + import sphinx + # NOTE(dhellmann): Newer versions of Sphinx have moved the apidoc + # module into sphinx.ext and the API is slightly different (the + # function expects sys.argv[1:] instead of sys.argv[:]. So, figure + # out where we can import it from and set a flag so we can invoke + # it properly. See this change in sphinx for details: + # https://github.com/sphinx-doc/sphinx/commit/87630c8ae8bff8c0e23187676e6343d8903003a6 + try: + from sphinx.ext import apidoc + apidoc_use_padding = False + except ImportError: + from sphinx import apidoc + apidoc_use_padding = True + from sphinx import application + from sphinx import setup_command +except Exception as e: + # NOTE(dhellmann): During the installation of docutils, setuptools + # tries to import pbr code to find the egg_info.writer hooks. That + # imports this module, which imports sphinx, which imports + # docutils, which is being installed. Because docutils uses 2to3 + # to convert its code during installation under python 3, the + # import fails, but it fails with an error other than ImportError + # (today it's a NameError on StandardError, an exception base + # class). Convert the exception type here so it can be caught in + # packaging.py where we try to determine if we can import and use + # sphinx by importing this module. See bug #1403510 for details. + raise ImportError(str(e)) +from pbr import git +from pbr import options +from pbr import version + + +_deprecated_options = ['autodoc_tree_index_modules', 'autodoc_index_modules', + 'autodoc_tree_excludes', 'autodoc_exclude_modules'] +_deprecated_envs = ['AUTODOC_TREE_INDEX_MODULES', 'AUTODOC_INDEX_MODULES'] +_rst_template = """%(heading)s +%(underline)s + +.. automodule:: %(module)s + :members: + :undoc-members: + :show-inheritance: +""" + + +def _find_modules(arg, dirname, files): + for filename in files: + if filename.endswith('.py') and filename != '__init__.py': + arg["%s.%s" % (dirname.replace('/', '.'), + filename[:-3])] = True + + +class LocalBuildDoc(setup_command.BuildDoc): + + builders = ['html'] + command_name = 'build_sphinx' + sphinx_initialized = False + + def _get_source_dir(self): + option_dict = self.distribution.get_option_dict('build_sphinx') + pbr_option_dict = self.distribution.get_option_dict('pbr') + _, api_doc_dir = pbr_option_dict.get('api_doc_dir', (None, 'api')) + if 'source_dir' in option_dict: + source_dir = os.path.join(option_dict['source_dir'][1], + api_doc_dir) + else: + source_dir = 'doc/source/' + api_doc_dir + if not os.path.exists(source_dir): + os.makedirs(source_dir) + return source_dir + + def generate_autoindex(self, excluded_modules=None): + log.info("[pbr] Autodocumenting from %s" + % os.path.abspath(os.curdir)) + modules = {} + source_dir = self._get_source_dir() + for pkg in self.distribution.packages: + if '.' not in pkg: + for dirpath, dirnames, files in os.walk(pkg): + _find_modules(modules, dirpath, files) + + def include(module): + return not any(fnmatch.fnmatch(module, pat) + for pat in excluded_modules) + + module_list = sorted(mod for mod in modules.keys() if include(mod)) + autoindex_filename = os.path.join(source_dir, 'autoindex.rst') + with open(autoindex_filename, 'w') as autoindex: + autoindex.write(""".. toctree:: + :maxdepth: 1 + +""") + for module in module_list: + output_filename = os.path.join(source_dir, + "%s.rst" % module) + heading = "The :mod:`%s` Module" % module + underline = "=" * len(heading) + values = dict(module=module, heading=heading, + underline=underline) + + log.info("[pbr] Generating %s" + % output_filename) + with open(output_filename, 'w') as output_file: + output_file.write(_rst_template % values) + autoindex.write(" %s.rst\n" % module) + + def _sphinx_tree(self): + source_dir = self._get_source_dir() + cmd = ['-H', 'Modules', '-o', source_dir, '.'] + if apidoc_use_padding: + cmd.insert(0, 'apidoc') + apidoc.main(cmd + self.autodoc_tree_excludes) + + def _sphinx_run(self): + if not self.verbose: + status_stream = cStringIO.StringIO() + else: + status_stream = sys.stdout + confoverrides = {} + if self.project: + confoverrides['project'] = self.project + if self.version: + confoverrides['version'] = self.version + if self.release: + confoverrides['release'] = self.release + if self.today: + confoverrides['today'] = self.today + if self.sphinx_initialized: + confoverrides['suppress_warnings'] = [ + 'app.add_directive', 'app.add_role', + 'app.add_generic_role', 'app.add_node', + 'image.nonlocal_uri', + ] + app = application.Sphinx( + self.source_dir, self.config_dir, + self.builder_target_dir, self.doctree_dir, + self.builder, confoverrides, status_stream, + freshenv=self.fresh_env, warningiserror=self.warning_is_error) + self.sphinx_initialized = True + + try: + app.build(force_all=self.all_files) + except Exception as err: + from docutils import utils + if isinstance(err, utils.SystemMessage): + sys.stder.write('reST markup error:\n') + sys.stderr.write(err.args[0].encode('ascii', + 'backslashreplace')) + sys.stderr.write('\n') + else: + raise + + if self.link_index: + src = app.config.master_doc + app.builder.out_suffix + dst = app.builder.get_outfilename('index') + os.symlink(src, dst) + + def run(self): + option_dict = self.distribution.get_option_dict('pbr') + + # TODO(stephenfin): Remove this (and the entire file) when 5.0 is + # released + warn_opts = set(option_dict.keys()).intersection(_deprecated_options) + warn_env = list(filter(lambda x: os.getenv(x), _deprecated_envs)) + if warn_opts or warn_env: + msg = ('The autodoc and autodoc_tree features are deprecated in ' + '4.2 and will be removed in a future release. You should ' + 'use the sphinxcontrib-apidoc Sphinx extension instead. ' + 'Refer to the pbr documentation for more information.') + if warn_opts: + msg += ' Deprecated options: %s' % list(warn_opts) + if warn_env: + msg += ' Deprecated environment variables: %s' % warn_env + + log.warn(msg) + + if git._git_is_installed(): + git.write_git_changelog(option_dict=option_dict) + git.generate_authors(option_dict=option_dict) + tree_index = options.get_boolean_option(option_dict, + 'autodoc_tree_index_modules', + 'AUTODOC_TREE_INDEX_MODULES') + auto_index = options.get_boolean_option(option_dict, + 'autodoc_index_modules', + 'AUTODOC_INDEX_MODULES') + if not os.getenv('SPHINX_DEBUG'): + # NOTE(afazekas): These options can be used together, + # but they do a very similar thing in a different way + if tree_index: + self._sphinx_tree() + if auto_index: + self.generate_autoindex( + set(option_dict.get( + "autodoc_exclude_modules", + [None, ""])[1].split())) + + self.finalize_options() + + is_multibuilder_sphinx = version.SemanticVersion.from_pip_string( + sphinx.__version__) >= version.SemanticVersion(1, 6) + + # TODO(stephenfin): Remove support for Sphinx < 1.6 in 4.0 + if not is_multibuilder_sphinx: + log.warn('[pbr] Support for Sphinx < 1.6 will be dropped in ' + 'pbr 4.0. Upgrade to Sphinx 1.6+') + + # TODO(stephenfin): Remove this at the next MAJOR version bump + if self.builders != ['html']: + log.warn("[pbr] Sphinx 1.6 added native support for " + "specifying multiple builders in the " + "'[sphinx_build] builder' configuration option, " + "found in 'setup.cfg'. As a result, the " + "'[sphinx_build] builders' option has been " + "deprecated and will be removed in pbr 4.0. Migrate " + "to the 'builder' configuration option.") + if is_multibuilder_sphinx: + self.builder = self.builders + + if is_multibuilder_sphinx: + # Sphinx >= 1.6 + return setup_command.BuildDoc.run(self) + + # Sphinx < 1.6 + for builder in self.builders: + self.builder = builder + self.finalize_options() + self._sphinx_run() + + def initialize_options(self): + # Not a new style class, super keyword does not work. + setup_command.BuildDoc.initialize_options(self) + + # NOTE(dstanek): exclude setup.py from the autodoc tree index + # builds because all projects will have an issue with it + self.autodoc_tree_excludes = ['setup.py'] + + def finalize_options(self): + from pbr import util + + # Not a new style class, super keyword does not work. + setup_command.BuildDoc.finalize_options(self) + + # Handle builder option from command line - override cfg + option_dict = self.distribution.get_option_dict('build_sphinx') + if 'command line' in option_dict.get('builder', [[]])[0]: + self.builders = option_dict['builder'][1] + # Allow builders to be configurable - as a comma separated list. + if not isinstance(self.builders, list) and self.builders: + self.builders = self.builders.split(',') + + self.project = self.distribution.get_name() + self.version = self.distribution.get_version() + self.release = self.distribution.get_version() + + # NOTE(dstanek): check for autodoc tree exclusion overrides + # in the setup.cfg + opt = 'autodoc_tree_excludes' + option_dict = self.distribution.get_option_dict('pbr') + if opt in option_dict: + self.autodoc_tree_excludes = util.split_multiline( + option_dict[opt][1]) + + # handle Sphinx < 1.5.0 + if not hasattr(self, 'warning_is_error'): + self.warning_is_error = False diff --git a/libs/pbr/cmd/__init__.py b/libs/pbr/cmd/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/pbr/cmd/main.py b/libs/pbr/cmd/main.py new file mode 100644 index 00000000..29cd61d7 --- /dev/null +++ b/libs/pbr/cmd/main.py @@ -0,0 +1,112 @@ +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse +import json +import sys + +import pkg_resources + +import pbr.version + + +def _get_metadata(package_name): + try: + return json.loads( + pkg_resources.get_distribution( + package_name).get_metadata('pbr.json')) + except pkg_resources.DistributionNotFound: + raise Exception('Package {0} not installed'.format(package_name)) + except Exception: + return None + + +def get_sha(args): + sha = _get_info(args.name)['sha'] + if sha: + print(sha) + + +def get_info(args): + print("{name}\t{version}\t{released}\t{sha}".format( + **_get_info(args.name))) + + +def _get_info(name): + metadata = _get_metadata(name) + version = pkg_resources.get_distribution(name).version + if metadata: + if metadata['is_release']: + released = 'released' + else: + released = 'pre-release' + sha = metadata['git_version'] + else: + version_parts = version.split('.') + if version_parts[-1].startswith('g'): + sha = version_parts[-1][1:] + released = 'pre-release' + else: + sha = "" + released = "released" + for part in version_parts: + if not part.isdigit(): + released = "pre-release" + return dict(name=name, version=version, sha=sha, released=released) + + +def freeze(args): + sorted_dists = sorted(pkg_resources.working_set, + key=lambda dist: dist.project_name.lower()) + for dist in sorted_dists: + info = _get_info(dist.project_name) + output = "{name}=={version}".format(**info) + if info['sha']: + output += " # git sha {sha}".format(**info) + print(output) + + +def main(): + parser = argparse.ArgumentParser( + description='pbr: Python Build Reasonableness') + parser.add_argument( + '-v', '--version', action='version', + version=str(pbr.version.VersionInfo('pbr'))) + + subparsers = parser.add_subparsers( + title='commands', description='valid commands', help='additional help') + + cmd_sha = subparsers.add_parser('sha', help='print sha of package') + cmd_sha.set_defaults(func=get_sha) + cmd_sha.add_argument('name', help='package to print sha of') + + cmd_info = subparsers.add_parser( + 'info', help='print version info for package') + cmd_info.set_defaults(func=get_info) + cmd_info.add_argument('name', help='package to print info of') + + cmd_freeze = subparsers.add_parser( + 'freeze', help='print version info for all installed packages') + cmd_freeze.set_defaults(func=freeze) + + args = parser.parse_args() + try: + args.func(args) + except Exception as e: + print(e) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/libs/pbr/core.py b/libs/pbr/core.py new file mode 100644 index 00000000..645a2ef1 --- /dev/null +++ b/libs/pbr/core.py @@ -0,0 +1,145 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Copyright (C) 2013 Association of Universities for Research in Astronomy +# (AURA) +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# +# 3. The name of AURA and its representatives may not be used to +# endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR IMPLIED +# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +# DAMAGE. + +import logging +import os +import sys +import warnings + +from distutils import errors + +from pbr import util + + +if sys.version_info[0] == 3: + string_type = str + integer_types = (int,) +else: + string_type = basestring # noqa + integer_types = (int, long) # noqa + + +def pbr(dist, attr, value): + """Implements the actual pbr setup() keyword. + + When used, this should be the only keyword in your setup() aside from + `setup_requires`. + + If given as a string, the value of pbr is assumed to be the relative path + to the setup.cfg file to use. Otherwise, if it evaluates to true, it + simply assumes that pbr should be used, and the default 'setup.cfg' is + used. + + This works by reading the setup.cfg file, parsing out the supported + metadata and command options, and using them to rebuild the + `DistributionMetadata` object and set the newly added command options. + + The reason for doing things this way is that a custom `Distribution` class + will not play nicely with setup_requires; however, this implementation may + not work well with distributions that do use a `Distribution` subclass. + """ + + if not value: + return + if isinstance(value, string_type): + path = os.path.abspath(value) + else: + path = os.path.abspath('setup.cfg') + if not os.path.exists(path): + raise errors.DistutilsFileError( + 'The setup.cfg file %s does not exist.' % path) + + # Converts the setup.cfg file to setup() arguments + try: + attrs = util.cfg_to_args(path, dist.script_args) + except Exception: + e = sys.exc_info()[1] + # NB: This will output to the console if no explicit logging has + # been setup - but thats fine, this is a fatal distutils error, so + # being pretty isn't the #1 goal.. being diagnosable is. + logging.exception('Error parsing') + raise errors.DistutilsSetupError( + 'Error parsing %s: %s: %s' % (path, e.__class__.__name__, e)) + + # There are some metadata fields that are only supported by + # setuptools and not distutils, and hence are not in + # dist.metadata. We are OK to write these in. For gory details + # see + # https://github.com/pypa/setuptools/pull/1343 + _DISTUTILS_UNSUPPORTED_METADATA = ( + 'long_description_content_type', 'project_urls', 'provides_extras' + ) + + # Repeat some of the Distribution initialization code with the newly + # provided attrs + if attrs: + # Skips 'options' and 'licence' support which are rarely used; may + # add back in later if demanded + for key, val in attrs.items(): + if hasattr(dist.metadata, 'set_' + key): + getattr(dist.metadata, 'set_' + key)(val) + elif hasattr(dist.metadata, key): + setattr(dist.metadata, key, val) + elif hasattr(dist, key): + setattr(dist, key, val) + elif key in _DISTUTILS_UNSUPPORTED_METADATA: + setattr(dist.metadata, key, val) + else: + msg = 'Unknown distribution option: %s' % repr(key) + warnings.warn(msg) + + # Re-finalize the underlying Distribution + try: + super(dist.__class__, dist).finalize_options() + except TypeError: + # If dist is not declared as a new-style class (with object as + # a subclass) then super() will not work on it. This is the case + # for Python 2. In that case, fall back to doing this the ugly way + dist.__class__.__bases__[-1].finalize_options(dist) + + # This bit comes out of distribute/setuptools + if isinstance(dist.metadata.version, integer_types + (float,)): + # Some people apparently take "version number" too literally :) + dist.metadata.version = str(dist.metadata.version) diff --git a/libs/pbr/extra_files.py b/libs/pbr/extra_files.py new file mode 100644 index 00000000..a72db0c1 --- /dev/null +++ b/libs/pbr/extra_files.py @@ -0,0 +1,35 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from distutils import errors +import os + +_extra_files = [] + + +def get_extra_files(): + global _extra_files + return _extra_files + + +def set_extra_files(extra_files): + # Let's do a sanity check + for filename in extra_files: + if not os.path.exists(filename): + raise errors.DistutilsFileError( + '%s from the extra_files option in setup.cfg does not ' + 'exist' % filename) + global _extra_files + _extra_files[:] = extra_files[:] diff --git a/libs/pbr/find_package.py b/libs/pbr/find_package.py new file mode 100644 index 00000000..717e93da --- /dev/null +++ b/libs/pbr/find_package.py @@ -0,0 +1,29 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os + +import setuptools + + +def smart_find_packages(package_list): + """Run find_packages the way we intend.""" + packages = [] + for pkg in package_list.strip().split("\n"): + pkg_path = pkg.replace('.', os.path.sep) + packages.append(pkg) + packages.extend(['%s.%s' % (pkg, f) + for f in setuptools.find_packages(pkg_path)]) + return "\n".join(set(packages)) diff --git a/libs/pbr/git.py b/libs/pbr/git.py new file mode 100644 index 00000000..6e18adae --- /dev/null +++ b/libs/pbr/git.py @@ -0,0 +1,331 @@ +# Copyright 2011 OpenStack Foundation +# Copyright 2012-2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import unicode_literals + +import distutils.errors +from distutils import log +import errno +import io +import os +import re +import subprocess +import time + +import pkg_resources + +from pbr import options +from pbr import version + + +def _run_shell_command(cmd, throw_on_error=False, buffer=True, env=None): + if buffer: + out_location = subprocess.PIPE + err_location = subprocess.PIPE + else: + out_location = None + err_location = None + + newenv = os.environ.copy() + if env: + newenv.update(env) + + output = subprocess.Popen(cmd, + stdout=out_location, + stderr=err_location, + env=newenv) + out = output.communicate() + if output.returncode and throw_on_error: + raise distutils.errors.DistutilsError( + "%s returned %d" % (cmd, output.returncode)) + if len(out) == 0 or not out[0] or not out[0].strip(): + return '' + # Since we don't control the history, and forcing users to rebase arbitrary + # history to fix utf8 issues is harsh, decode with replace. + return out[0].strip().decode('utf-8', 'replace') + + +def _run_git_command(cmd, git_dir, **kwargs): + if not isinstance(cmd, (list, tuple)): + cmd = [cmd] + return _run_shell_command( + ['git', '--git-dir=%s' % git_dir] + cmd, **kwargs) + + +def _get_git_directory(): + try: + return _run_shell_command(['git', 'rev-parse', '--git-dir']) + except OSError as e: + if e.errno == errno.ENOENT: + # git not installed. + return '' + raise + + +def _git_is_installed(): + try: + # We cannot use 'which git' as it may not be available + # in some distributions, So just try 'git --version' + # to see if we run into trouble + _run_shell_command(['git', '--version']) + except OSError: + return False + return True + + +def _get_highest_tag(tags): + """Find the highest tag from a list. + + Pass in a list of tag strings and this will return the highest + (latest) as sorted by the pkg_resources version parser. + """ + return max(tags, key=pkg_resources.parse_version) + + +def _find_git_files(dirname='', git_dir=None): + """Behave like a file finder entrypoint plugin. + + We don't actually use the entrypoints system for this because it runs + at absurd times. We only want to do this when we are building an sdist. + """ + file_list = [] + if git_dir is None: + git_dir = _run_git_functions() + if git_dir: + log.info("[pbr] In git context, generating filelist from git") + file_list = _run_git_command(['ls-files', '-z'], git_dir) + # Users can fix utf8 issues locally with a single commit, so we are + # strict here. + file_list = file_list.split(b'\x00'.decode('utf-8')) + return [f for f in file_list if f] + + +def _get_raw_tag_info(git_dir): + describe = _run_git_command(['describe', '--always'], git_dir) + if "-" in describe: + return describe.rsplit("-", 2)[-2] + if "." in describe: + return 0 + return None + + +def get_is_release(git_dir): + return _get_raw_tag_info(git_dir) == 0 + + +def _run_git_functions(): + git_dir = None + if _git_is_installed(): + git_dir = _get_git_directory() + return git_dir or None + + +def get_git_short_sha(git_dir=None): + """Return the short sha for this repo, if it exists.""" + if not git_dir: + git_dir = _run_git_functions() + if git_dir: + return _run_git_command( + ['log', '-n1', '--pretty=format:%h'], git_dir) + return None + + +def _clean_changelog_message(msg): + """Cleans any instances of invalid sphinx wording. + + This escapes/removes any instances of invalid characters + that can be interpreted by sphinx as a warning or error + when translating the Changelog into an HTML file for + documentation building within projects. + + * Escapes '_' which is interpreted as a link + * Escapes '*' which is interpreted as a new line + * Escapes '`' which is interpreted as a literal + """ + + msg = msg.replace('*', '\*') + msg = msg.replace('_', '\_') + msg = msg.replace('`', '\`') + + return msg + + +def _iter_changelog(changelog): + """Convert a oneline log iterator to formatted strings. + + :param changelog: An iterator of one line log entries like + that given by _iter_log_oneline. + :return: An iterator over (release, formatted changelog) tuples. + """ + first_line = True + current_release = None + yield current_release, "CHANGES\n=======\n\n" + for hash, tags, msg in changelog: + if tags: + current_release = _get_highest_tag(tags) + underline = len(current_release) * '-' + if not first_line: + yield current_release, '\n' + yield current_release, ( + "%(tag)s\n%(underline)s\n\n" % + dict(tag=current_release, underline=underline)) + + if not msg.startswith("Merge "): + if msg.endswith("."): + msg = msg[:-1] + msg = _clean_changelog_message(msg) + yield current_release, "* %(msg)s\n" % dict(msg=msg) + first_line = False + + +def _iter_log_oneline(git_dir=None): + """Iterate over --oneline log entries if possible. + + This parses the output into a structured form but does not apply + presentation logic to the output - making it suitable for different + uses. + + :return: An iterator of (hash, tags_set, 1st_line) tuples, or None if + changelog generation is disabled / not available. + """ + if git_dir is None: + git_dir = _get_git_directory() + if not git_dir: + return [] + return _iter_log_inner(git_dir) + + +def _is_valid_version(candidate): + try: + version.SemanticVersion.from_pip_string(candidate) + return True + except ValueError: + return False + + +def _iter_log_inner(git_dir): + """Iterate over --oneline log entries. + + This parses the output intro a structured form but does not apply + presentation logic to the output - making it suitable for different + uses. + + :return: An iterator of (hash, tags_set, 1st_line) tuples. + """ + log.info('[pbr] Generating ChangeLog') + log_cmd = ['log', '--decorate=full', '--format=%h%x00%s%x00%d'] + changelog = _run_git_command(log_cmd, git_dir) + for line in changelog.split('\n'): + line_parts = line.split('\x00') + if len(line_parts) != 3: + continue + sha, msg, refname = line_parts + tags = set() + + # refname can be: + # + # HEAD, tag: refs/tags/1.4.0, refs/remotes/origin/master, \ + # refs/heads/master + # refs/tags/1.3.4 + if "refs/tags/" in refname: + refname = refname.strip()[1:-1] # remove wrapping ()'s + # If we start with "tag: refs/tags/1.2b1, tag: refs/tags/1.2" + # The first split gives us "['', '1.2b1, tag:', '1.2']" + # Which is why we do the second split below on the comma + for tag_string in refname.split("refs/tags/")[1:]: + # git tag does not allow : or " " in tag names, so we split + # on ", " which is the separator between elements + candidate = tag_string.split(", ")[0] + if _is_valid_version(candidate): + tags.add(candidate) + + yield sha, tags, msg + + +def write_git_changelog(git_dir=None, dest_dir=os.path.curdir, + option_dict=None, changelog=None): + """Write a changelog based on the git changelog.""" + start = time.time() + if not option_dict: + option_dict = {} + should_skip = options.get_boolean_option(option_dict, 'skip_changelog', + 'SKIP_WRITE_GIT_CHANGELOG') + if should_skip: + return + if not changelog: + changelog = _iter_log_oneline(git_dir=git_dir) + if changelog: + changelog = _iter_changelog(changelog) + if not changelog: + return + new_changelog = os.path.join(dest_dir, 'ChangeLog') + # If there's already a ChangeLog and it's not writable, just use it + if (os.path.exists(new_changelog) + and not os.access(new_changelog, os.W_OK)): + log.info('[pbr] ChangeLog not written (file already' + ' exists and it is not writeable)') + return + log.info('[pbr] Writing ChangeLog') + with io.open(new_changelog, "w", encoding="utf-8") as changelog_file: + for release, content in changelog: + changelog_file.write(content) + stop = time.time() + log.info('[pbr] ChangeLog complete (%0.1fs)' % (stop - start)) + + +def generate_authors(git_dir=None, dest_dir='.', option_dict=dict()): + """Create AUTHORS file using git commits.""" + should_skip = options.get_boolean_option(option_dict, 'skip_authors', + 'SKIP_GENERATE_AUTHORS') + if should_skip: + return + start = time.time() + old_authors = os.path.join(dest_dir, 'AUTHORS.in') + new_authors = os.path.join(dest_dir, 'AUTHORS') + # If there's already an AUTHORS file and it's not writable, just use it + if (os.path.exists(new_authors) + and not os.access(new_authors, os.W_OK)): + return + log.info('[pbr] Generating AUTHORS') + ignore_emails = '((jenkins|zuul)@review|infra@lists|jenkins@openstack)' + if git_dir is None: + git_dir = _get_git_directory() + if git_dir: + authors = [] + + # don't include jenkins email address in AUTHORS file + git_log_cmd = ['log', '--format=%aN <%aE>'] + authors += _run_git_command(git_log_cmd, git_dir).split('\n') + authors = [a for a in authors if not re.search(ignore_emails, a)] + + # get all co-authors from commit messages + co_authors_out = _run_git_command('log', git_dir) + co_authors = re.findall('Co-authored-by:.+', co_authors_out, + re.MULTILINE) + co_authors = [signed.split(":", 1)[1].strip() + for signed in co_authors if signed] + + authors += co_authors + authors = sorted(set(authors)) + + with open(new_authors, 'wb') as new_authors_fh: + if os.path.exists(old_authors): + with open(old_authors, "rb") as old_authors_fh: + new_authors_fh.write(old_authors_fh.read()) + new_authors_fh.write(('\n'.join(authors) + '\n') + .encode('utf-8')) + stop = time.time() + log.info('[pbr] AUTHORS complete (%0.1fs)' % (stop - start)) diff --git a/libs/pbr/hooks/__init__.py b/libs/pbr/hooks/__init__.py new file mode 100644 index 00000000..f0056c0e --- /dev/null +++ b/libs/pbr/hooks/__init__.py @@ -0,0 +1,28 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from pbr.hooks import backwards +from pbr.hooks import commands +from pbr.hooks import files +from pbr.hooks import metadata + + +def setup_hook(config): + """Filter config parsed from a setup.cfg to inject our defaults.""" + metadata_config = metadata.MetadataConfig(config) + metadata_config.run() + backwards.BackwardsCompatConfig(config).run() + commands.CommandsConfig(config).run() + files.FilesConfig(config, metadata_config.get_name()).run() diff --git a/libs/pbr/hooks/backwards.py b/libs/pbr/hooks/backwards.py new file mode 100644 index 00000000..01f07ab8 --- /dev/null +++ b/libs/pbr/hooks/backwards.py @@ -0,0 +1,33 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from pbr.hooks import base +from pbr import packaging + + +class BackwardsCompatConfig(base.BaseConfig): + + section = 'backwards_compat' + + def hook(self): + self.config['include_package_data'] = 'True' + packaging.append_text_list( + self.config, 'dependency_links', + packaging.parse_dependency_links()) + packaging.append_text_list( + self.config, 'tests_require', + packaging.parse_requirements( + packaging.TEST_REQUIREMENTS_FILES, + strip_markers=True)) diff --git a/libs/pbr/hooks/base.py b/libs/pbr/hooks/base.py new file mode 100644 index 00000000..6672a362 --- /dev/null +++ b/libs/pbr/hooks/base.py @@ -0,0 +1,34 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +class BaseConfig(object): + + section = None + + def __init__(self, config): + self._global_config = config + self.config = self._global_config.get(self.section, dict()) + self.pbr_config = config.get('pbr', dict()) + + def run(self): + self.hook() + self.save() + + def hook(self): + pass + + def save(self): + self._global_config[self.section] = self.config diff --git a/libs/pbr/hooks/commands.py b/libs/pbr/hooks/commands.py new file mode 100644 index 00000000..aa4db704 --- /dev/null +++ b/libs/pbr/hooks/commands.py @@ -0,0 +1,66 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os + +from setuptools.command import easy_install + +from pbr.hooks import base +from pbr import options +from pbr import packaging + + +class CommandsConfig(base.BaseConfig): + + section = 'global' + + def __init__(self, config): + super(CommandsConfig, self).__init__(config) + self.commands = self.config.get('commands', "") + + def save(self): + self.config['commands'] = self.commands + super(CommandsConfig, self).save() + + def add_command(self, command): + self.commands = "%s\n%s" % (self.commands, command) + + def hook(self): + self.add_command('pbr.packaging.LocalEggInfo') + self.add_command('pbr.packaging.LocalSDist') + self.add_command('pbr.packaging.LocalInstallScripts') + self.add_command('pbr.packaging.LocalDevelop') + self.add_command('pbr.packaging.LocalRPMVersion') + self.add_command('pbr.packaging.LocalDebVersion') + if os.name != 'nt': + easy_install.get_script_args = packaging.override_get_script_args + + if packaging.have_sphinx(): + self.add_command('pbr.builddoc.LocalBuildDoc') + + if os.path.exists('.testr.conf') and packaging.have_testr(): + # There is a .testr.conf file. We want to use it. + self.add_command('pbr.packaging.TestrTest') + elif self.config.get('nosetests', False) and packaging.have_nose(): + # We seem to still have nose configured + self.add_command('pbr.packaging.NoseTest') + + use_egg = options.get_boolean_option( + self.pbr_config, 'use-egg', 'PBR_USE_EGG') + # We always want non-egg install unless explicitly requested + if 'manpages' in self.pbr_config or not use_egg: + self.add_command('pbr.packaging.LocalInstall') + else: + self.add_command('pbr.packaging.InstallWithGit') diff --git a/libs/pbr/hooks/files.py b/libs/pbr/hooks/files.py new file mode 100644 index 00000000..48bf9e31 --- /dev/null +++ b/libs/pbr/hooks/files.py @@ -0,0 +1,103 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +import sys + +from pbr import find_package +from pbr.hooks import base + + +def get_manpath(): + manpath = 'share/man' + if os.path.exists(os.path.join(sys.prefix, 'man')): + # This works around a bug with install where it expects every node + # in the relative data directory to be an actual directory, since at + # least Debian derivatives (and probably other platforms as well) + # like to symlink Unixish /usr/local/man to /usr/local/share/man. + manpath = 'man' + return manpath + + +def get_man_section(section): + return os.path.join(get_manpath(), 'man%s' % section) + + +class FilesConfig(base.BaseConfig): + + section = 'files' + + def __init__(self, config, name): + super(FilesConfig, self).__init__(config) + self.name = name + self.data_files = self.config.get('data_files', '') + + def save(self): + self.config['data_files'] = self.data_files + super(FilesConfig, self).save() + + def expand_globs(self): + finished = [] + for line in self.data_files.split("\n"): + if line.rstrip().endswith('*') and '=' in line: + (target, source_glob) = line.split('=') + source_prefix = source_glob.strip()[:-1] + target = target.strip() + if not target.endswith(os.path.sep): + target += os.path.sep + for (dirpath, dirnames, fnames) in os.walk(source_prefix): + finished.append( + "%s = " % dirpath.replace(source_prefix, target)) + finished.extend( + [" %s" % os.path.join(dirpath, f) for f in fnames]) + else: + finished.append(line) + + self.data_files = "\n".join(finished) + + def add_man_path(self, man_path): + self.data_files = "%s\n%s =" % (self.data_files, man_path) + + def add_man_page(self, man_page): + self.data_files = "%s\n %s" % (self.data_files, man_page) + + def get_man_sections(self): + man_sections = dict() + manpages = self.pbr_config['manpages'] + for manpage in manpages.split(): + section_number = manpage.strip()[-1] + section = man_sections.get(section_number, list()) + section.append(manpage.strip()) + man_sections[section_number] = section + return man_sections + + def hook(self): + packages = self.config.get('packages', self.name).strip() + expanded = [] + for pkg in packages.split("\n"): + if os.path.isdir(pkg.strip()): + expanded.append(find_package.smart_find_packages(pkg.strip())) + + self.config['packages'] = "\n".join(expanded) + + self.expand_globs() + + if 'manpages' in self.pbr_config: + man_sections = self.get_man_sections() + for (section, pages) in man_sections.items(): + manpath = get_man_section(section) + self.add_man_path(manpath) + for page in pages: + self.add_man_page(page) diff --git a/libs/pbr/hooks/metadata.py b/libs/pbr/hooks/metadata.py new file mode 100644 index 00000000..3f65b6d7 --- /dev/null +++ b/libs/pbr/hooks/metadata.py @@ -0,0 +1,32 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from pbr.hooks import base +from pbr import packaging + + +class MetadataConfig(base.BaseConfig): + + section = 'metadata' + + def hook(self): + self.config['version'] = packaging.get_version( + self.config['name'], self.config.get('version', None)) + packaging.append_text_list( + self.config, 'requires_dist', + packaging.parse_requirements()) + + def get_name(self): + return self.config['name'] diff --git a/libs/pbr/options.py b/libs/pbr/options.py new file mode 100644 index 00000000..105b200e --- /dev/null +++ b/libs/pbr/options.py @@ -0,0 +1,53 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Copyright (C) 2013 Association of Universities for Research in Astronomy +# (AURA) +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# +# 3. The name of AURA and its representatives may not be used to +# endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR IMPLIED +# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +# DAMAGE. + +import os + + +TRUE_VALUES = ('true', '1', 'yes') + + +def get_boolean_option(option_dict, option_name, env_name): + return ((option_name in option_dict + and option_dict[option_name][1].lower() in TRUE_VALUES) or + str(os.getenv(env_name)).lower() in TRUE_VALUES) diff --git a/libs/pbr/packaging.py b/libs/pbr/packaging.py new file mode 100644 index 00000000..77a4b226 --- /dev/null +++ b/libs/pbr/packaging.py @@ -0,0 +1,855 @@ +# Copyright 2011 OpenStack Foundation +# Copyright 2012-2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Utilities with minimum-depends for use in setup.py +""" + +from __future__ import unicode_literals + +from distutils.command import install as du_install +from distutils import log +import email +import email.errors +import os +import re +import sys +import warnings + +import pkg_resources +import setuptools +from setuptools.command import develop +from setuptools.command import easy_install +from setuptools.command import egg_info +from setuptools.command import install +from setuptools.command import install_scripts +from setuptools.command import sdist + +from pbr import extra_files +from pbr import git +from pbr import options +import pbr.pbr_json +from pbr import testr_command +from pbr import version + +REQUIREMENTS_FILES = ('requirements.txt', 'tools/pip-requires') +PY_REQUIREMENTS_FILES = [x % sys.version_info[0] for x in ( + 'requirements-py%d.txt', 'tools/pip-requires-py%d')] +TEST_REQUIREMENTS_FILES = ('test-requirements.txt', 'tools/test-requires') + + +def get_requirements_files(): + files = os.environ.get("PBR_REQUIREMENTS_FILES") + if files: + return tuple(f.strip() for f in files.split(',')) + # Returns a list composed of: + # - REQUIREMENTS_FILES with -py2 or -py3 in the name + # (e.g. requirements-py3.txt) + # - REQUIREMENTS_FILES + + return PY_REQUIREMENTS_FILES + list(REQUIREMENTS_FILES) + + +def append_text_list(config, key, text_list): + """Append a \n separated list to possibly existing value.""" + new_value = [] + current_value = config.get(key, "") + if current_value: + new_value.append(current_value) + new_value.extend(text_list) + config[key] = '\n'.join(new_value) + + +def _any_existing(file_list): + return [f for f in file_list if os.path.exists(f)] + + +# Get requirements from the first file that exists +def get_reqs_from_files(requirements_files): + existing = _any_existing(requirements_files) + + # TODO(stephenfin): Remove this in pbr 6.0+ + deprecated = [f for f in existing if f in PY_REQUIREMENTS_FILES] + if deprecated: + warnings.warn('Support for \'-pyN\'-suffixed requirements files is ' + 'removed in pbr 5.0 and these files are now ignored. ' + 'Use environment markers instead. Conflicting files: ' + '%r' % deprecated, + DeprecationWarning) + + existing = [f for f in existing if f not in PY_REQUIREMENTS_FILES] + for requirements_file in existing: + with open(requirements_file, 'r') as fil: + return fil.read().split('\n') + + return [] + + +def parse_requirements(requirements_files=None, strip_markers=False): + + if requirements_files is None: + requirements_files = get_requirements_files() + + def egg_fragment(match): + # take a versioned egg fragment and return a + # versioned package requirement e.g. + # nova-1.2.3 becomes nova>=1.2.3 + return re.sub(r'([\w.]+)-([\w.-]+)', + r'\1>=\2', + match.groups()[-1]) + + requirements = [] + for line in get_reqs_from_files(requirements_files): + # Ignore comments + if (not line.strip()) or line.startswith('#'): + continue + + # Ignore index URL lines + if re.match(r'^\s*(-i|--index-url|--extra-index-url).*', line): + continue + + # Handle nested requirements files such as: + # -r other-requirements.txt + if line.startswith('-r'): + req_file = line.partition(' ')[2] + requirements += parse_requirements( + [req_file], strip_markers=strip_markers) + continue + + try: + project_name = pkg_resources.Requirement.parse(line).project_name + except ValueError: + project_name = None + + # For the requirements list, we need to inject only the portion + # after egg= so that distutils knows the package it's looking for + # such as: + # -e git://github.com/openstack/nova/master#egg=nova + # -e git://github.com/openstack/nova/master#egg=nova-1.2.3 + # -e git+https://foo.com/zipball#egg=bar&subdirectory=baz + if re.match(r'\s*-e\s+', line): + line = re.sub(r'\s*-e\s+.*#egg=([^&]+).*$', egg_fragment, line) + # such as: + # http://github.com/openstack/nova/zipball/master#egg=nova + # http://github.com/openstack/nova/zipball/master#egg=nova-1.2.3 + # git+https://foo.com/zipball#egg=bar&subdirectory=baz + elif re.match(r'\s*(https?|git(\+(https|ssh))?):', line): + line = re.sub(r'\s*(https?|git(\+(https|ssh))?):.*#egg=([^&]+).*$', + egg_fragment, line) + # -f lines are for index locations, and don't get used here + elif re.match(r'\s*-f\s+', line): + line = None + reason = 'Index Location' + + if line is not None: + line = re.sub('#.*$', '', line) + if strip_markers: + semi_pos = line.find(';') + if semi_pos < 0: + semi_pos = None + line = line[:semi_pos] + requirements.append(line) + else: + log.info( + '[pbr] Excluding %s: %s' % (project_name, reason)) + + return requirements + + +def parse_dependency_links(requirements_files=None): + if requirements_files is None: + requirements_files = get_requirements_files() + dependency_links = [] + # dependency_links inject alternate locations to find packages listed + # in requirements + for line in get_reqs_from_files(requirements_files): + # skip comments and blank lines + if re.match(r'(\s*#)|(\s*$)', line): + continue + # lines with -e or -f need the whole line, minus the flag + if re.match(r'\s*-[ef]\s+', line): + dependency_links.append(re.sub(r'\s*-[ef]\s+', '', line)) + # lines that are only urls can go in unmolested + elif re.match(r'\s*(https?|git(\+(https|ssh))?):', line): + dependency_links.append(line) + return dependency_links + + +class InstallWithGit(install.install): + """Extracts ChangeLog and AUTHORS from git then installs. + + This is useful for e.g. readthedocs where the package is + installed and then docs built. + """ + + command_name = 'install' + + def run(self): + _from_git(self.distribution) + return install.install.run(self) + + +class LocalInstall(install.install): + """Runs python setup.py install in a sensible manner. + + Force a non-egg installed in the manner of + single-version-externally-managed, which allows us to install manpages + and config files. + """ + + command_name = 'install' + + def run(self): + _from_git(self.distribution) + return du_install.install.run(self) + + +class TestrTest(testr_command.Testr): + """Make setup.py test do the right thing.""" + + command_name = 'test' + description = 'DEPRECATED: Run unit tests using testr' + + def run(self): + warnings.warn('testr integration is deprecated in pbr 4.2 and will ' + 'be removed in a future release. Please call your test ' + 'runner directly', + DeprecationWarning) + + # Can't use super - base class old-style class + testr_command.Testr.run(self) + + +class LocalRPMVersion(setuptools.Command): + __doc__ = """Output the rpm *compatible* version string of this package""" + description = __doc__ + + user_options = [] + command_name = "rpm_version" + + def run(self): + log.info("[pbr] Extracting rpm version") + name = self.distribution.get_name() + print(version.VersionInfo(name).semantic_version().rpm_string()) + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + +class LocalDebVersion(setuptools.Command): + __doc__ = """Output the deb *compatible* version string of this package""" + description = __doc__ + + user_options = [] + command_name = "deb_version" + + def run(self): + log.info("[pbr] Extracting deb version") + name = self.distribution.get_name() + print(version.VersionInfo(name).semantic_version().debian_string()) + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + +def have_testr(): + return testr_command.have_testr + + +try: + from nose import commands + + class NoseTest(commands.nosetests): + """Fallback test runner if testr is a no-go.""" + + command_name = 'test' + description = 'DEPRECATED: Run unit tests using nose' + + def run(self): + warnings.warn('nose integration in pbr is deprecated. Please use ' + 'the native nose setuptools configuration or call ' + 'nose directly', + DeprecationWarning) + + # Can't use super - base class old-style class + commands.nosetests.run(self) + + _have_nose = True + +except ImportError: + _have_nose = False + + +def have_nose(): + return _have_nose + +_wsgi_text = """#PBR Generated from %(group)r + +import threading + +from %(module_name)s import %(import_target)s + +if __name__ == "__main__": + import argparse + import socket + import sys + import wsgiref.simple_server as wss + + parser = argparse.ArgumentParser( + description=%(import_target)s.__doc__, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + usage='%%(prog)s [-h] [--port PORT] [--host IP] -- [passed options]') + parser.add_argument('--port', '-p', type=int, default=8000, + help='TCP port to listen on') + parser.add_argument('--host', '-b', default='', + help='IP to bind the server to') + parser.add_argument('args', + nargs=argparse.REMAINDER, + metavar='-- [passed options]', + help="'--' is the separator of the arguments used " + "to start the WSGI server and the arguments passed " + "to the WSGI application.") + args = parser.parse_args() + if args.args: + if args.args[0] == '--': + args.args.pop(0) + else: + parser.error("unrecognized arguments: %%s" %% ' '.join(args.args)) + sys.argv[1:] = args.args + server = wss.make_server(args.host, args.port, %(invoke_target)s()) + + print("*" * 80) + print("STARTING test server %(module_name)s.%(invoke_target)s") + url = "http://%%s:%%d/" %% (server.server_name, server.server_port) + print("Available at %%s" %% url) + print("DANGER! For testing only, do not use in production") + print("*" * 80) + sys.stdout.flush() + + server.serve_forever() +else: + application = None + app_lock = threading.Lock() + + with app_lock: + if application is None: + application = %(invoke_target)s() + +""" + +_script_text = """# PBR Generated from %(group)r + +import sys + +from %(module_name)s import %(import_target)s + + +if __name__ == "__main__": + sys.exit(%(invoke_target)s()) +""" + + +# the following allows us to specify different templates per entry +# point group when generating pbr scripts. +ENTRY_POINTS_MAP = { + 'console_scripts': _script_text, + 'gui_scripts': _script_text, + 'wsgi_scripts': _wsgi_text +} + + +def generate_script(group, entry_point, header, template): + """Generate the script based on the template. + + :param str group: + The entry-point group name, e.g., "console_scripts". + :param str header: + The first line of the script, e.g., "!#/usr/bin/env python". + :param str template: + The script template. + :returns: + The templated script content + :rtype: + str + """ + if not entry_point.attrs or len(entry_point.attrs) > 2: + raise ValueError("Script targets must be of the form " + "'func' or 'Class.class_method'.") + script_text = template % dict( + group=group, + module_name=entry_point.module_name, + import_target=entry_point.attrs[0], + invoke_target='.'.join(entry_point.attrs), + ) + return header + script_text + + +def override_get_script_args( + dist, executable=os.path.normpath(sys.executable), is_wininst=False): + """Override entrypoints console_script.""" + header = easy_install.get_script_header("", executable, is_wininst) + for group, template in ENTRY_POINTS_MAP.items(): + for name, ep in dist.get_entry_map(group).items(): + yield (name, generate_script(group, ep, header, template)) + + +class LocalDevelop(develop.develop): + + command_name = 'develop' + + def install_wrapper_scripts(self, dist): + if sys.platform == 'win32': + return develop.develop.install_wrapper_scripts(self, dist) + if not self.exclude_scripts: + for args in override_get_script_args(dist): + self.write_script(*args) + + +class LocalInstallScripts(install_scripts.install_scripts): + """Intercepts console scripts entry_points.""" + command_name = 'install_scripts' + + def _make_wsgi_scripts_only(self, dist, executable, is_wininst): + header = easy_install.get_script_header("", executable, is_wininst) + wsgi_script_template = ENTRY_POINTS_MAP['wsgi_scripts'] + for name, ep in dist.get_entry_map('wsgi_scripts').items(): + content = generate_script( + 'wsgi_scripts', ep, header, wsgi_script_template) + self.write_script(name, content) + + def run(self): + import distutils.command.install_scripts + + self.run_command("egg_info") + if self.distribution.scripts: + # run first to set up self.outfiles + distutils.command.install_scripts.install_scripts.run(self) + else: + self.outfiles = [] + + ei_cmd = self.get_finalized_command("egg_info") + dist = pkg_resources.Distribution( + ei_cmd.egg_base, + pkg_resources.PathMetadata(ei_cmd.egg_base, ei_cmd.egg_info), + ei_cmd.egg_name, ei_cmd.egg_version, + ) + bs_cmd = self.get_finalized_command('build_scripts') + executable = getattr( + bs_cmd, 'executable', easy_install.sys_executable) + is_wininst = getattr( + self.get_finalized_command("bdist_wininst"), '_is_running', False + ) + + if 'bdist_wheel' in self.distribution.have_run: + # We're building a wheel which has no way of generating mod_wsgi + # scripts for us. Let's build them. + # NOTE(sigmavirus24): This needs to happen here because, as the + # comment below indicates, no_ep is True when building a wheel. + self._make_wsgi_scripts_only(dist, executable, is_wininst) + + if self.no_ep: + # no_ep is True if we're installing into an .egg file or building + # a .whl file, in those cases, we do not want to build all of the + # entry-points listed for this package. + return + + if os.name != 'nt': + get_script_args = override_get_script_args + else: + get_script_args = easy_install.get_script_args + executable = '"%s"' % executable + + for args in get_script_args(dist, executable, is_wininst): + self.write_script(*args) + + +class LocalManifestMaker(egg_info.manifest_maker): + """Add any files that are in git and some standard sensible files.""" + + def _add_pbr_defaults(self): + for template_line in [ + 'include AUTHORS', + 'include ChangeLog', + 'exclude .gitignore', + 'exclude .gitreview', + 'global-exclude *.pyc' + ]: + self.filelist.process_template_line(template_line) + + def add_defaults(self): + """Add all the default files to self.filelist: + + Extends the functionality provided by distutils to also included + additional sane defaults, such as the ``AUTHORS`` and ``ChangeLog`` + files generated by *pbr*. + + Warns if (``README`` or ``README.txt``) or ``setup.py`` are missing; + everything else is optional. + """ + option_dict = self.distribution.get_option_dict('pbr') + + sdist.sdist.add_defaults(self) + self.filelist.append(self.template) + self.filelist.append(self.manifest) + self.filelist.extend(extra_files.get_extra_files()) + should_skip = options.get_boolean_option(option_dict, 'skip_git_sdist', + 'SKIP_GIT_SDIST') + if not should_skip: + rcfiles = git._find_git_files() + if rcfiles: + self.filelist.extend(rcfiles) + elif os.path.exists(self.manifest): + self.read_manifest() + ei_cmd = self.get_finalized_command('egg_info') + self._add_pbr_defaults() + self.filelist.include_pattern("*", prefix=ei_cmd.egg_info) + + +class LocalEggInfo(egg_info.egg_info): + """Override the egg_info command to regenerate SOURCES.txt sensibly.""" + + command_name = 'egg_info' + + def find_sources(self): + """Generate SOURCES.txt only if there isn't one already. + + If we are in an sdist command, then we always want to update + SOURCES.txt. If we are not in an sdist command, then it doesn't + matter one flip, and is actually destructive. + However, if we're in a git context, it's always the right thing to do + to recreate SOURCES.txt + """ + manifest_filename = os.path.join(self.egg_info, "SOURCES.txt") + if (not os.path.exists(manifest_filename) or + os.path.exists('.git') or + 'sdist' in sys.argv): + log.info("[pbr] Processing SOURCES.txt") + mm = LocalManifestMaker(self.distribution) + mm.manifest = manifest_filename + mm.run() + self.filelist = mm.filelist + else: + log.info("[pbr] Reusing existing SOURCES.txt") + self.filelist = egg_info.FileList() + for entry in open(manifest_filename, 'r').read().split('\n'): + self.filelist.append(entry) + + +def _from_git(distribution): + option_dict = distribution.get_option_dict('pbr') + changelog = git._iter_log_oneline() + if changelog: + changelog = git._iter_changelog(changelog) + git.write_git_changelog(option_dict=option_dict, changelog=changelog) + git.generate_authors(option_dict=option_dict) + + +class LocalSDist(sdist.sdist): + """Builds the ChangeLog and Authors files from VC first.""" + + command_name = 'sdist' + + def checking_reno(self): + """Ensure reno is installed and configured. + + We can't run reno-based commands if reno isn't installed/available, and + don't want to if the user isn't using it. + """ + if hasattr(self, '_has_reno'): + return self._has_reno + + option_dict = self.distribution.get_option_dict('pbr') + should_skip = options.get_boolean_option(option_dict, 'skip_reno', + 'SKIP_GENERATE_RENO') + if should_skip: + self._has_reno = False + return False + + try: + # versions of reno witout this module will not have the required + # feature, hence the import + from reno import setup_command # noqa + except ImportError: + log.info('[pbr] reno was not found or is too old. Skipping ' + 'release notes') + self._has_reno = False + return False + + conf, output_file, cache_file = setup_command.load_config( + self.distribution) + + if not os.path.exists(os.path.join(conf.reporoot, conf.notespath)): + log.info('[pbr] reno does not appear to be configured. Skipping ' + 'release notes') + self._has_reno = False + return False + + self._files = [output_file, cache_file] + + log.info('[pbr] Generating release notes') + self._has_reno = True + + return True + + sub_commands = [('build_reno', checking_reno)] + sdist.sdist.sub_commands + + def run(self): + _from_git(self.distribution) + # sdist.sdist is an old style class, can't use super() + sdist.sdist.run(self) + + def make_distribution(self): + # This is included in make_distribution because setuptools doesn't use + # 'get_file_list'. As such, this is the only hook point that runs after + # the commands in 'sub_commands' + if self.checking_reno(): + self.filelist.extend(self._files) + self.filelist.sort() + sdist.sdist.make_distribution(self) + +try: + from pbr import builddoc + _have_sphinx = True + # Import the symbols from their new home so the package API stays + # compatible. + LocalBuildDoc = builddoc.LocalBuildDoc +except ImportError: + _have_sphinx = False + LocalBuildDoc = None + + +def have_sphinx(): + return _have_sphinx + + +def _get_increment_kwargs(git_dir, tag): + """Calculate the sort of semver increment needed from git history. + + Every commit from HEAD to tag is consider for Sem-Ver metadata lines. + See the pbr docs for their syntax. + + :return: a dict of kwargs for passing into SemanticVersion.increment. + """ + result = {} + if tag: + version_spec = tag + "..HEAD" + else: + version_spec = "HEAD" + # Get the raw body of the commit messages so that we don't have to + # parse out any formatting whitespace and to avoid user settings on + # git log output affecting out ability to have working sem ver headers. + changelog = git._run_git_command(['log', '--pretty=%B', version_spec], + git_dir) + header_len = len('sem-ver:') + commands = [line[header_len:].strip() for line in changelog.split('\n') + if line.lower().startswith('sem-ver:')] + symbols = set() + for command in commands: + symbols.update([symbol.strip() for symbol in command.split(',')]) + + def _handle_symbol(symbol, symbols, impact): + if symbol in symbols: + result[impact] = True + symbols.discard(symbol) + _handle_symbol('bugfix', symbols, 'patch') + _handle_symbol('feature', symbols, 'minor') + _handle_symbol('deprecation', symbols, 'minor') + _handle_symbol('api-break', symbols, 'major') + for symbol in symbols: + log.info('[pbr] Unknown Sem-Ver symbol %r' % symbol) + # We don't want patch in the kwargs since it is not a keyword argument - + # its the default minimum increment. + result.pop('patch', None) + return result + + +def _get_revno_and_last_tag(git_dir): + """Return the commit data about the most recent tag. + + We use git-describe to find this out, but if there are no + tags then we fall back to counting commits since the beginning + of time. + """ + changelog = git._iter_log_oneline(git_dir=git_dir) + row_count = 0 + for row_count, (ignored, tag_set, ignored) in enumerate(changelog): + version_tags = set() + semver_to_tag = dict() + for tag in list(tag_set): + try: + semver = version.SemanticVersion.from_pip_string(tag) + semver_to_tag[semver] = tag + version_tags.add(semver) + except Exception: + pass + if version_tags: + return semver_to_tag[max(version_tags)], row_count + return "", row_count + + +def _get_version_from_git_target(git_dir, target_version): + """Calculate a version from a target version in git_dir. + + This is used for untagged versions only. A new version is calculated as + necessary based on git metadata - distance to tags, current hash, contents + of commit messages. + + :param git_dir: The git directory we're working from. + :param target_version: If None, the last tagged version (or 0 if there are + no tags yet) is incremented as needed to produce an appropriate target + version following semver rules. Otherwise target_version is used as a + constraint - if semver rules would result in a newer version then an + exception is raised. + :return: A semver version object. + """ + tag, distance = _get_revno_and_last_tag(git_dir) + last_semver = version.SemanticVersion.from_pip_string(tag or '0') + if distance == 0: + new_version = last_semver + else: + new_version = last_semver.increment( + **_get_increment_kwargs(git_dir, tag)) + if target_version is not None and new_version > target_version: + raise ValueError( + "git history requires a target version of %(new)s, but target " + "version is %(target)s" % + dict(new=new_version, target=target_version)) + if distance == 0: + return last_semver + new_dev = new_version.to_dev(distance) + if target_version is not None: + target_dev = target_version.to_dev(distance) + if target_dev > new_dev: + return target_dev + return new_dev + + +def _get_version_from_git(pre_version=None): + """Calculate a version string from git. + + If the revision is tagged, return that. Otherwise calculate a semantic + version description of the tree. + + The number of revisions since the last tag is included in the dev counter + in the version for untagged versions. + + :param pre_version: If supplied use this as the target version rather than + inferring one from the last tag + commit messages. + """ + git_dir = git._run_git_functions() + if git_dir: + try: + tagged = git._run_git_command( + ['describe', '--exact-match'], git_dir, + throw_on_error=True).replace('-', '.') + target_version = version.SemanticVersion.from_pip_string(tagged) + except Exception: + if pre_version: + # not released yet - use pre_version as the target + target_version = version.SemanticVersion.from_pip_string( + pre_version) + else: + # not released yet - just calculate from git history + target_version = None + result = _get_version_from_git_target(git_dir, target_version) + return result.release_string() + # If we don't know the version, return an empty string so at least + # the downstream users of the value always have the same type of + # object to work with. + try: + return unicode() + except NameError: + return '' + + +def _get_version_from_pkg_metadata(package_name): + """Get the version from package metadata if present. + + This looks for PKG-INFO if present (for sdists), and if not looks + for METADATA (for wheels) and failing that will return None. + """ + pkg_metadata_filenames = ['PKG-INFO', 'METADATA'] + pkg_metadata = {} + for filename in pkg_metadata_filenames: + try: + pkg_metadata_file = open(filename, 'r') + except (IOError, OSError): + continue + try: + pkg_metadata = email.message_from_file(pkg_metadata_file) + except email.errors.MessageError: + continue + + # Check to make sure we're in our own dir + if pkg_metadata.get('Name', None) != package_name: + return None + return pkg_metadata.get('Version', None) + + +def get_version(package_name, pre_version=None): + """Get the version of the project. + + First, try getting it from PKG-INFO or METADATA, if it exists. If it does, + that means we're in a distribution tarball or that install has happened. + Otherwise, if there is no PKG-INFO or METADATA file, pull the version + from git. + + We do not support setup.py version sanity in git archive tarballs, nor do + we support packagers directly sucking our git repo into theirs. We expect + that a source tarball be made from our git repo - or that if someone wants + to make a source tarball from a fork of our repo with additional tags in it + that they understand and desire the results of doing that. + + :param pre_version: The version field from setup.cfg - if set then this + version will be the next release. + """ + version = os.environ.get( + "PBR_VERSION", + os.environ.get("OSLO_PACKAGE_VERSION", None)) + if version: + return version + version = _get_version_from_pkg_metadata(package_name) + if version: + return version + version = _get_version_from_git(pre_version) + # Handle http://bugs.python.org/issue11638 + # version will either be an empty unicode string or a valid + # unicode version string, but either way it's unicode and needs to + # be encoded. + if sys.version_info[0] == 2: + version = version.encode('utf-8') + if version: + return version + raise Exception("Versioning for this project requires either an sdist" + " tarball, or access to an upstream git repository." + " It's also possible that there is a mismatch between" + " the package name in setup.cfg and the argument given" + " to pbr.version.VersionInfo. Project name {name} was" + " given, but was not able to be found.".format( + name=package_name)) + + +# This is added because pbr uses pbr to install itself. That means that +# any changes to the egg info writer entrypoints must be forward and +# backward compatible. This maintains the pbr.packaging.write_pbr_json +# path. +write_pbr_json = pbr.pbr_json.write_pbr_json diff --git a/libs/pbr/pbr_json.py b/libs/pbr/pbr_json.py new file mode 100644 index 00000000..08c3da22 --- /dev/null +++ b/libs/pbr/pbr_json.py @@ -0,0 +1,34 @@ +# Copyright 2011 OpenStack Foundation +# Copyright 2012-2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +from pbr import git + + +def write_pbr_json(cmd, basename, filename): + if not hasattr(cmd.distribution, 'pbr') or not cmd.distribution.pbr: + return + git_dir = git._run_git_functions() + if not git_dir: + return + values = dict() + git_version = git.get_git_short_sha(git_dir) + is_release = git.get_is_release(git_dir) + if git_version is not None: + values['git_version'] = git_version + values['is_release'] = is_release + cmd.write_file('pbr', filename, json.dumps(values, sort_keys=True)) diff --git a/libs/pbr/sphinxext.py b/libs/pbr/sphinxext.py new file mode 100644 index 00000000..ef613052 --- /dev/null +++ b/libs/pbr/sphinxext.py @@ -0,0 +1,99 @@ +# Copyright 2018 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os.path + +from six.moves import configparser +from sphinx.util import logging + +import pbr.version + +_project = None +logger = logging.getLogger(__name__) + + +def _find_setup_cfg(srcdir): + """Find the 'setup.cfg' file, if it exists. + + This assumes we're using 'doc/source' for documentation, but also allows + for single level 'doc' paths. + """ + # TODO(stephenfin): Are we sure that this will always exist, e.g. for + # an sdist or wheel? Perhaps we should check for 'PKG-INFO' or + # 'METADATA' files, a la 'pbr.packaging._get_version_from_pkg_metadata' + for path in [ + os.path.join(srcdir, os.pardir, 'setup.cfg'), + os.path.join(srcdir, os.pardir, os.pardir, 'setup.cfg')]: + if os.path.exists(path): + return path + + return None + + +def _get_project_name(srcdir): + """Return string name of project name, or None. + + This extracts metadata from 'setup.cfg'. We don't rely on + distutils/setuptools as we don't want to actually install the package + simply to build docs. + """ + global _project + + if _project is None: + parser = configparser.ConfigParser() + + path = _find_setup_cfg(srcdir) + if not path or not parser.read(path): + logger.info('Could not find a setup.cfg to extract project name ' + 'from') + return None + + try: + # for project name we use the name in setup.cfg, but if the + # length is longer then 32 we use summary. Otherwise thAe + # menu rendering looks brolen + project = parser.get('metadata', 'name') + if len(project.split()) == 1 and len(project) > 32: + project = parser.get('metadata', 'summary') + except configparser.Error: + logger.info('Could not extract project metadata from setup.cfg') + return None + + _project = project + + return _project + + +def _builder_inited(app): + # TODO(stephenfin): Once Sphinx 1.8 is released, we should move the below + # to a 'config-inited' handler + + project_name = _get_project_name(app.srcdir) + try: + version_info = pbr.version.VersionInfo(project_name) + except Exception: + version_info = None + + if version_info and not app.config.version and not app.config.release: + app.config.version = version_info.canonical_version_string() + app.config.release = version_info.version_string_with_vcs() + + +def setup(app): + app.connect('builder-inited', _builder_inited) + return { + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/libs/pbr/testr_command.py b/libs/pbr/testr_command.py new file mode 100644 index 00000000..d143565f --- /dev/null +++ b/libs/pbr/testr_command.py @@ -0,0 +1,167 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Copyright (c) 2013 Testrepository Contributors +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +"""setuptools/distutils command to run testr via setup.py + +PBR will hook in the Testr class to provide "setup.py test" when +.testr.conf is present in the repository (see pbr/hooks/commands.py). + +If we are activated but testrepository is not installed, we provide a +sensible error. + +You can pass --coverage which will also export PYTHON='coverage run +--source ' and automatically combine the coverage from +each testr backend test runner after the run completes. + +""" + +from distutils import cmd +import distutils.errors +import logging +import os +import sys +import warnings + +logger = logging.getLogger(__name__) + + +class TestrReal(cmd.Command): + + description = "DEPRECATED: Run unit tests using testr" + + user_options = [ + ('coverage', None, "Replace PYTHON with coverage and merge coverage " + "from each testr worker."), + ('testr-args=', 't', "Run 'testr' with these args"), + ('omit=', 'o', "Files to omit from coverage calculations"), + ('coverage-package-name=', None, "Use this name to select packages " + "for coverage (one or more, " + "comma-separated)"), + ('slowest', None, "Show slowest test times after tests complete."), + ('no-parallel', None, "Run testr serially"), + ('log-level=', 'l', "Log level (default: info)"), + ] + + boolean_options = ['coverage', 'slowest', 'no_parallel'] + + def _run_testr(self, *args): + logger.debug("_run_testr called with args = %r", args) + return commands.run_argv([sys.argv[0]] + list(args), + sys.stdin, sys.stdout, sys.stderr) + + def initialize_options(self): + self.testr_args = None + self.coverage = None + self.omit = "" + self.slowest = None + self.coverage_package_name = None + self.no_parallel = None + self.log_level = 'info' + + def finalize_options(self): + self.log_level = getattr( + logging, + self.log_level.upper(), + logging.INFO) + logging.basicConfig(level=self.log_level) + logger.debug("finalize_options called") + if self.testr_args is None: + self.testr_args = [] + else: + self.testr_args = self.testr_args.split() + if self.omit: + self.omit = "--omit=%s" % self.omit + logger.debug("finalize_options: self.__dict__ = %r", self.__dict__) + + def run(self): + """Set up testr repo, then run testr.""" + logger.debug("run called") + + warnings.warn('testr integration in pbr is deprecated. Please use ' + 'the \'testr\' setup command or call testr directly', + DeprecationWarning) + + if not os.path.isdir(".testrepository"): + self._run_testr("init") + + if self.coverage: + self._coverage_before() + if not self.no_parallel: + testr_ret = self._run_testr("run", "--parallel", *self.testr_args) + else: + testr_ret = self._run_testr("run", *self.testr_args) + if testr_ret: + raise distutils.errors.DistutilsError( + "testr failed (%d)" % testr_ret) + if self.slowest: + print("Slowest Tests") + self._run_testr("slowest") + if self.coverage: + self._coverage_after() + + def _coverage_before(self): + logger.debug("_coverage_before called") + package = self.distribution.get_name() + if package.startswith('python-'): + package = package[7:] + + # Use this as coverage package name + if self.coverage_package_name: + package = self.coverage_package_name + options = "--source %s --parallel-mode" % package + os.environ['PYTHON'] = ("coverage run %s" % options) + logger.debug("os.environ['PYTHON'] = %r", os.environ['PYTHON']) + + def _coverage_after(self): + logger.debug("_coverage_after called") + os.system("coverage combine") + os.system("coverage html -d ./cover %s" % self.omit) + os.system("coverage xml -o ./cover/coverage.xml %s" % self.omit) + + +class TestrFake(cmd.Command): + description = "Run unit tests using testr" + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + print("Install testrepository to run 'testr' command properly.") + + +try: + from testrepository import commands + have_testr = True + Testr = TestrReal +except ImportError: + have_testr = False + Testr = TestrFake diff --git a/libs/pbr/tests/__init__.py b/libs/pbr/tests/__init__.py new file mode 100644 index 00000000..583e0c6b --- /dev/null +++ b/libs/pbr/tests/__init__.py @@ -0,0 +1,26 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import testscenarios + + +def load_tests(loader, standard_tests, pattern): + # top level directory cached on loader instance + this_dir = os.path.dirname(__file__) + package_tests = loader.discover(start_dir=this_dir, pattern=pattern) + result = loader.suiteClass() + result.addTests(testscenarios.generate_scenarios(standard_tests)) + result.addTests(testscenarios.generate_scenarios(package_tests)) + return result diff --git a/libs/pbr/tests/base.py b/libs/pbr/tests/base.py new file mode 100644 index 00000000..9c409b0a --- /dev/null +++ b/libs/pbr/tests/base.py @@ -0,0 +1,221 @@ +# Copyright 2010-2011 OpenStack Foundation +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# Copyright (C) 2013 Association of Universities for Research in Astronomy +# (AURA) +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# +# 3. The name of AURA and its representatives may not be used to +# endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR IMPLIED +# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + +"""Common utilities used in testing""" + +import os +import shutil +import subprocess +import sys + +import fixtures +import testresources +import testtools +from testtools import content + +from pbr import options + + +class DiveDir(fixtures.Fixture): + """Dive into given directory and return back on cleanup. + + :ivar path: The target directory. + """ + + def __init__(self, path): + self.path = path + + def setUp(self): + super(DiveDir, self).setUp() + self.addCleanup(os.chdir, os.getcwd()) + os.chdir(self.path) + + +class BaseTestCase(testtools.TestCase, testresources.ResourcedTestCase): + + def setUp(self): + super(BaseTestCase, self).setUp() + test_timeout = os.environ.get('OS_TEST_TIMEOUT', 30) + try: + test_timeout = int(test_timeout) + except ValueError: + # If timeout value is invalid, fail hard. + print("OS_TEST_TIMEOUT set to invalid value" + " defaulting to no timeout") + test_timeout = 0 + if test_timeout > 0: + self.useFixture(fixtures.Timeout(test_timeout, gentle=True)) + + if os.environ.get('OS_STDOUT_CAPTURE') in options.TRUE_VALUES: + stdout = self.useFixture(fixtures.StringStream('stdout')).stream + self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout)) + if os.environ.get('OS_STDERR_CAPTURE') in options.TRUE_VALUES: + stderr = self.useFixture(fixtures.StringStream('stderr')).stream + self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) + self.log_fixture = self.useFixture( + fixtures.FakeLogger('pbr')) + + # Older git does not have config --local, so create a temporary home + # directory to permit using git config --global without stepping on + # developer configuration. + self.useFixture(fixtures.TempHomeDir()) + self.useFixture(fixtures.NestedTempfile()) + self.useFixture(fixtures.FakeLogger()) + # TODO(lifeless) we should remove PBR_VERSION from the environment. + # rather than setting it, because thats not representative - we need to + # test non-preversioned codepaths too! + self.useFixture(fixtures.EnvironmentVariable('PBR_VERSION', '0.0')) + + self.temp_dir = self.useFixture(fixtures.TempDir()).path + self.package_dir = os.path.join(self.temp_dir, 'testpackage') + shutil.copytree(os.path.join(os.path.dirname(__file__), 'testpackage'), + self.package_dir) + self.addCleanup(os.chdir, os.getcwd()) + os.chdir(self.package_dir) + self.addCleanup(self._discard_testpackage) + # Tests can opt into non-PBR_VERSION by setting preversioned=False as + # an attribute. + if not getattr(self, 'preversioned', True): + self.useFixture(fixtures.EnvironmentVariable('PBR_VERSION')) + setup_cfg_path = os.path.join(self.package_dir, 'setup.cfg') + with open(setup_cfg_path, 'rt') as cfg: + content = cfg.read() + content = content.replace(u'version = 0.1.dev', u'') + with open(setup_cfg_path, 'wt') as cfg: + cfg.write(content) + + def _discard_testpackage(self): + # Remove pbr.testpackage from sys.modules so that it can be freshly + # re-imported by the next test + for k in list(sys.modules): + if (k == 'pbr_testpackage' or + k.startswith('pbr_testpackage.')): + del sys.modules[k] + + def run_pbr(self, *args, **kwargs): + return self._run_cmd('pbr', args, **kwargs) + + def run_setup(self, *args, **kwargs): + return self._run_cmd(sys.executable, ('setup.py',) + args, **kwargs) + + def _run_cmd(self, cmd, args=[], allow_fail=True, cwd=None): + """Run a command in the root of the test working copy. + + Runs a command, with the given argument list, in the root of the test + working copy--returns the stdout and stderr streams and the exit code + from the subprocess. + + :param cwd: If falsy run within the test package dir, otherwise run + within the named path. + """ + cwd = cwd or self.package_dir + result = _run_cmd([cmd] + list(args), cwd=cwd) + if result[2] and not allow_fail: + raise Exception("Command failed retcode=%s" % result[2]) + return result + + +class CapturedSubprocess(fixtures.Fixture): + """Run a process and capture its output. + + :attr stdout: The output (a string). + :attr stderr: The standard error (a string). + :attr returncode: The return code of the process. + + Note that stdout and stderr are decoded from the bytestrings subprocess + returns using error=replace. + """ + + def __init__(self, label, *args, **kwargs): + """Create a CapturedSubprocess. + + :param label: A label for the subprocess in the test log. E.g. 'foo'. + :param *args: The *args to pass to Popen. + :param **kwargs: The **kwargs to pass to Popen. + """ + super(CapturedSubprocess, self).__init__() + self.label = label + self.args = args + self.kwargs = kwargs + self.kwargs['stderr'] = subprocess.PIPE + self.kwargs['stdin'] = subprocess.PIPE + self.kwargs['stdout'] = subprocess.PIPE + + def setUp(self): + super(CapturedSubprocess, self).setUp() + proc = subprocess.Popen(*self.args, **self.kwargs) + out, err = proc.communicate() + self.out = out.decode('utf-8', 'replace') + self.err = err.decode('utf-8', 'replace') + self.addDetail(self.label + '-stdout', content.text_content(self.out)) + self.addDetail(self.label + '-stderr', content.text_content(self.err)) + self.returncode = proc.returncode + if proc.returncode: + raise AssertionError('Failed process %s' % proc.returncode) + self.addCleanup(delattr, self, 'out') + self.addCleanup(delattr, self, 'err') + self.addCleanup(delattr, self, 'returncode') + + +def _run_cmd(args, cwd): + """Run the command args in cwd. + + :param args: The command to run e.g. ['git', 'status'] + :param cwd: The directory to run the comamnd in. + :return: ((stdout, stderr), returncode) + """ + p = subprocess.Popen( + args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, cwd=cwd) + streams = tuple(s.decode('latin1').strip() for s in p.communicate()) + for stream_content in streams: + print(stream_content) + return (streams) + (p.returncode,) + + +def _config_git(): + _run_cmd( + ['git', 'config', '--global', 'user.email', 'example@example.com'], + None) + _run_cmd( + ['git', 'config', '--global', 'user.name', 'OpenStack Developer'], + None) + _run_cmd( + ['git', 'config', '--global', 'user.signingkey', + 'example@example.com'], None) diff --git a/libs/pbr/tests/test_commands.py b/libs/pbr/tests/test_commands.py new file mode 100644 index 00000000..51e27116 --- /dev/null +++ b/libs/pbr/tests/test_commands.py @@ -0,0 +1,84 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Copyright (C) 2013 Association of Universities for Research in Astronomy +# (AURA) +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# +# 3. The name of AURA and its representatives may not be used to +# endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR IMPLIED +# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + +from testtools import content + +from pbr.tests import base + + +class TestCommands(base.BaseTestCase): + def test_custom_build_py_command(self): + """Test custom build_py command. + + Test that a custom subclass of the build_py command runs when listed in + the commands [global] option, rather than the normal build command. + """ + + stdout, stderr, return_code = self.run_setup('build_py') + self.addDetail('stdout', content.text_content(stdout)) + self.addDetail('stderr', content.text_content(stderr)) + self.assertIn('Running custom build_py command.', stdout) + self.assertEqual(0, return_code) + + def test_custom_deb_version_py_command(self): + """Test custom deb_version command.""" + stdout, stderr, return_code = self.run_setup('deb_version') + self.addDetail('stdout', content.text_content(stdout)) + self.addDetail('stderr', content.text_content(stderr)) + self.assertIn('Extracting deb version', stdout) + self.assertEqual(0, return_code) + + def test_custom_rpm_version_py_command(self): + """Test custom rpm_version command.""" + stdout, stderr, return_code = self.run_setup('rpm_version') + self.addDetail('stdout', content.text_content(stdout)) + self.addDetail('stderr', content.text_content(stderr)) + self.assertIn('Extracting rpm version', stdout) + self.assertEqual(0, return_code) + + def test_freeze_command(self): + """Test that freeze output is sorted in a case-insensitive manner.""" + stdout, stderr, return_code = self.run_pbr('freeze') + self.assertEqual(0, return_code) + pkgs = [] + for l in stdout.split('\n'): + pkgs.append(l.split('==')[0].lower()) + pkgs_sort = sorted(pkgs[:]) + self.assertEqual(pkgs_sort, pkgs) diff --git a/libs/pbr/tests/test_core.py b/libs/pbr/tests/test_core.py new file mode 100644 index 00000000..0ee6f532 --- /dev/null +++ b/libs/pbr/tests/test_core.py @@ -0,0 +1,151 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Copyright (C) 2013 Association of Universities for Research in Astronomy +# (AURA) +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# +# 3. The name of AURA and its representatives may not be used to +# endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR IMPLIED +# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + +import glob +import os +import tarfile + +import fixtures + +from pbr.tests import base + + +class TestCore(base.BaseTestCase): + + cmd_names = ('pbr_test_cmd', 'pbr_test_cmd_with_class') + + def check_script_install(self, install_stdout): + for cmd_name in self.cmd_names: + install_txt = 'Installing %s script to %s' % (cmd_name, + self.temp_dir) + self.assertIn(install_txt, install_stdout) + + cmd_filename = os.path.join(self.temp_dir, cmd_name) + + script_txt = open(cmd_filename, 'r').read() + self.assertNotIn('pkg_resources', script_txt) + + stdout, _, return_code = self._run_cmd(cmd_filename) + self.assertIn("PBR", stdout) + + def test_setup_py_keywords(self): + """setup.py --keywords. + + Test that the `./setup.py --keywords` command returns the correct + value without balking. + """ + + self.run_setup('egg_info') + stdout, _, _ = self.run_setup('--keywords') + assert stdout == 'packaging,distutils,setuptools' + + def test_setup_py_build_sphinx(self): + stdout, _, return_code = self.run_setup('build_sphinx') + self.assertEqual(0, return_code) + + def test_sdist_extra_files(self): + """Test that the extra files are correctly added.""" + + stdout, _, return_code = self.run_setup('sdist', '--formats=gztar') + + # There can be only one + try: + tf_path = glob.glob(os.path.join('dist', '*.tar.gz'))[0] + except IndexError: + assert False, 'source dist not found' + + tf = tarfile.open(tf_path) + names = ['/'.join(p.split('/')[1:]) for p in tf.getnames()] + + self.assertIn('extra-file.txt', names) + + def test_console_script_install(self): + """Test that we install a non-pkg-resources console script.""" + + if os.name == 'nt': + self.skipTest('Windows support is passthrough') + + stdout, _, return_code = self.run_setup( + 'install_scripts', '--install-dir=%s' % self.temp_dir) + + self.useFixture( + fixtures.EnvironmentVariable('PYTHONPATH', '.')) + + self.check_script_install(stdout) + + def test_console_script_develop(self): + """Test that we develop a non-pkg-resources console script.""" + + if os.name == 'nt': + self.skipTest('Windows support is passthrough') + + self.useFixture( + fixtures.EnvironmentVariable( + 'PYTHONPATH', ".:%s" % self.temp_dir)) + + stdout, _, return_code = self.run_setup( + 'develop', '--install-dir=%s' % self.temp_dir) + + self.check_script_install(stdout) + + +class TestGitSDist(base.BaseTestCase): + + def setUp(self): + super(TestGitSDist, self).setUp() + + stdout, _, return_code = self._run_cmd('git', ('init',)) + if return_code: + self.skipTest("git not installed") + + stdout, _, return_code = self._run_cmd('git', ('add', '.')) + stdout, _, return_code = self._run_cmd( + 'git', ('commit', '-m', 'Turn this into a git repo')) + + stdout, _, return_code = self.run_setup('sdist', '--formats=gztar') + + def test_sdist_git_extra_files(self): + """Test that extra files found in git are correctly added.""" + # There can be only one + tf_path = glob.glob(os.path.join('dist', '*.tar.gz'))[0] + tf = tarfile.open(tf_path) + names = ['/'.join(p.split('/')[1:]) for p in tf.getnames()] + + self.assertIn('git-extra-file.txt', names) diff --git a/libs/pbr/tests/test_files.py b/libs/pbr/tests/test_files.py new file mode 100644 index 00000000..e60b6ca7 --- /dev/null +++ b/libs/pbr/tests/test_files.py @@ -0,0 +1,78 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import print_function + +import os + +import fixtures + +from pbr.hooks import files +from pbr.tests import base + + +class FilesConfigTest(base.BaseTestCase): + + def setUp(self): + super(FilesConfigTest, self).setUp() + + pkg_fixture = fixtures.PythonPackage( + "fake_package", [ + ("fake_module.py", b""), + ("other_fake_module.py", b""), + ]) + self.useFixture(pkg_fixture) + pkg_etc = os.path.join(pkg_fixture.base, 'etc') + pkg_sub = os.path.join(pkg_etc, 'sub') + subpackage = os.path.join( + pkg_fixture.base, 'fake_package', 'subpackage') + os.makedirs(pkg_sub) + os.makedirs(subpackage) + with open(os.path.join(pkg_etc, "foo"), 'w') as foo_file: + foo_file.write("Foo Data") + with open(os.path.join(pkg_sub, "bar"), 'w') as foo_file: + foo_file.write("Bar Data") + with open(os.path.join(subpackage, "__init__.py"), 'w') as foo_file: + foo_file.write("# empty") + + self.useFixture(base.DiveDir(pkg_fixture.base)) + + def test_implicit_auto_package(self): + config = dict( + files=dict( + ) + ) + files.FilesConfig(config, 'fake_package').run() + self.assertIn('subpackage', config['files']['packages']) + + def test_auto_package(self): + config = dict( + files=dict( + packages='fake_package', + ) + ) + files.FilesConfig(config, 'fake_package').run() + self.assertIn('subpackage', config['files']['packages']) + + def test_data_files_globbing(self): + config = dict( + files=dict( + data_files="\n etc/pbr = etc/*" + ) + ) + files.FilesConfig(config, 'fake_package').run() + self.assertIn( + '\netc/pbr/ = \n etc/foo\netc/pbr/sub = \n etc/sub/bar', + config['files']['data_files']) diff --git a/libs/pbr/tests/test_hooks.py b/libs/pbr/tests/test_hooks.py new file mode 100644 index 00000000..3f747904 --- /dev/null +++ b/libs/pbr/tests/test_hooks.py @@ -0,0 +1,75 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Copyright (C) 2013 Association of Universities for Research in Astronomy +# (AURA) +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# +# 3. The name of AURA and its representatives may not be used to +# endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR IMPLIED +# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + +import os + +from testtools import matchers +from testtools import skipUnless + +from pbr import testr_command +from pbr.tests import base +from pbr.tests import util + + +class TestHooks(base.BaseTestCase): + def setUp(self): + super(TestHooks, self).setUp() + with util.open_config( + os.path.join(self.package_dir, 'setup.cfg')) as cfg: + cfg.set('global', 'setup-hooks', + 'pbr_testpackage._setup_hooks.test_hook_1\n' + 'pbr_testpackage._setup_hooks.test_hook_2') + + def test_global_setup_hooks(self): + """Test setup_hooks. + + Test that setup_hooks listed in the [global] section of setup.cfg are + executed in order. + """ + + stdout, _, return_code = self.run_setup('egg_info') + assert 'test_hook_1\ntest_hook_2' in stdout + assert return_code == 0 + + @skipUnless(testr_command.have_testr, "testrepository not available") + def test_custom_commands_known(self): + stdout, _, return_code = self.run_setup('--help-commands') + self.assertFalse(return_code) + self.assertThat(stdout, matchers.Contains(" testr ")) diff --git a/libs/pbr/tests/test_integration.py b/libs/pbr/tests/test_integration.py new file mode 100644 index 00000000..8e96f21f --- /dev/null +++ b/libs/pbr/tests/test_integration.py @@ -0,0 +1,269 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os.path +import shlex +import sys + +import fixtures +import testtools +import textwrap + +from pbr.tests import base +from pbr.tests import test_packaging + +PIPFLAGS = shlex.split(os.environ.get('PIPFLAGS', '')) +PIPVERSION = os.environ.get('PIPVERSION', 'pip') +PBRVERSION = os.environ.get('PBRVERSION', 'pbr') +REPODIR = os.environ.get('REPODIR', '') +WHEELHOUSE = os.environ.get('WHEELHOUSE', '') +PIP_CMD = ['-m', 'pip'] + PIPFLAGS + ['install', '-f', WHEELHOUSE] +PROJECTS = shlex.split(os.environ.get('PROJECTS', '')) +PBR_ROOT = os.path.abspath(os.path.join(__file__, '..', '..', '..')) + + +def all_projects(): + if not REPODIR: + return + # Future: make this path parameterisable. + excludes = set(['tempest', 'requirements']) + for name in PROJECTS: + name = name.strip() + short_name = name.split('/')[-1] + try: + with open(os.path.join( + REPODIR, short_name, 'setup.py'), 'rt') as f: + if 'pbr' not in f.read(): + continue + except IOError: + continue + if short_name in excludes: + continue + yield (short_name, dict(name=name, short_name=short_name)) + + +class TestIntegration(base.BaseTestCase): + + scenarios = list(all_projects()) + + def setUp(self): + # Integration tests need a higher default - big repos can be slow to + # clone, particularly under guest load. + env = fixtures.EnvironmentVariable( + 'OS_TEST_TIMEOUT', os.environ.get('OS_TEST_TIMEOUT', '600')) + with env: + super(TestIntegration, self).setUp() + base._config_git() + + @testtools.skipUnless( + os.environ.get('PBR_INTEGRATION', None) == '1', + 'integration tests not enabled') + def test_integration(self): + # Test that we can: + # - run sdist from the repo in a venv + # - install the resulting tarball in a new venv + # - pip install the repo + # - pip install -e the repo + # We don't break these into separate tests because we'd need separate + # source dirs to isolate from side effects of running pip, and the + # overheads of setup would start to beat the benefits of parallelism. + self.useFixture(base.CapturedSubprocess( + 'sync-req', + ['python', 'update.py', os.path.join(REPODIR, self.short_name)], + cwd=os.path.join(REPODIR, 'requirements'))) + self.useFixture(base.CapturedSubprocess( + 'commit-requirements', + 'git diff --quiet || git commit -amrequirements', + cwd=os.path.join(REPODIR, self.short_name), shell=True)) + path = os.path.join( + self.useFixture(fixtures.TempDir()).path, 'project') + self.useFixture(base.CapturedSubprocess( + 'clone', + ['git', 'clone', os.path.join(REPODIR, self.short_name), path])) + venv = self.useFixture( + test_packaging.Venv('sdist', + modules=['pip', 'wheel', PBRVERSION], + pip_cmd=PIP_CMD)) + python = venv.python + self.useFixture(base.CapturedSubprocess( + 'sdist', [python, 'setup.py', 'sdist'], cwd=path)) + venv = self.useFixture( + test_packaging.Venv('tarball', + modules=['pip', 'wheel', PBRVERSION], + pip_cmd=PIP_CMD)) + python = venv.python + filename = os.path.join( + path, 'dist', os.listdir(os.path.join(path, 'dist'))[0]) + self.useFixture(base.CapturedSubprocess( + 'tarball', [python] + PIP_CMD + [filename])) + venv = self.useFixture( + test_packaging.Venv('install-git', + modules=['pip', 'wheel', PBRVERSION], + pip_cmd=PIP_CMD)) + root = venv.path + python = venv.python + self.useFixture(base.CapturedSubprocess( + 'install-git', [python] + PIP_CMD + ['git+file://' + path])) + if self.short_name == 'nova': + found = False + for _, _, filenames in os.walk(root): + if 'migrate.cfg' in filenames: + found = True + self.assertTrue(found) + venv = self.useFixture( + test_packaging.Venv('install-e', + modules=['pip', 'wheel', PBRVERSION], + pip_cmd=PIP_CMD)) + root = venv.path + python = venv.python + self.useFixture(base.CapturedSubprocess( + 'install-e', [python] + PIP_CMD + ['-e', path])) + + +class TestInstallWithoutPbr(base.BaseTestCase): + + @testtools.skipUnless( + os.environ.get('PBR_INTEGRATION', None) == '1', + 'integration tests not enabled') + def test_install_without_pbr(self): + # Test easy-install of a thing that depends on a thing using pbr + tempdir = self.useFixture(fixtures.TempDir()).path + # A directory containing sdists of the things we're going to depend on + # in using-package. + dist_dir = os.path.join(tempdir, 'distdir') + os.mkdir(dist_dir) + self._run_cmd(sys.executable, ('setup.py', 'sdist', '-d', dist_dir), + allow_fail=False, cwd=PBR_ROOT) + # testpkg - this requires a pbr-using package + test_pkg_dir = os.path.join(tempdir, 'testpkg') + os.mkdir(test_pkg_dir) + pkgs = { + 'pkgTest': { + 'setup.py': textwrap.dedent("""\ + #!/usr/bin/env python + import setuptools + setuptools.setup( + name = 'pkgTest', + tests_require = ['pkgReq'], + test_suite='pkgReq' + ) + """), + 'setup.cfg': textwrap.dedent("""\ + [easy_install] + find_links = %s + """ % dist_dir)}, + 'pkgReq': { + 'requirements.txt': textwrap.dedent("""\ + pbr + """), + 'pkgReq/__init__.py': textwrap.dedent("""\ + print("FakeTest loaded and ran") + """)}, + } + pkg_dirs = self.useFixture( + test_packaging.CreatePackages(pkgs)).package_dirs + test_pkg_dir = pkg_dirs['pkgTest'] + req_pkg_dir = pkg_dirs['pkgReq'] + + self._run_cmd(sys.executable, ('setup.py', 'sdist', '-d', dist_dir), + allow_fail=False, cwd=req_pkg_dir) + # A venv to test within + venv = self.useFixture(test_packaging.Venv('nopbr', ['pip', 'wheel'])) + python = venv.python + # Run the depending script + self.useFixture(base.CapturedSubprocess( + 'nopbr', [python] + ['setup.py', 'test'], cwd=test_pkg_dir)) + + +class TestMarkersPip(base.BaseTestCase): + + scenarios = [ + ('pip-1.5', {'modules': ['pip>=1.5,<1.6']}), + ('pip-6.0', {'modules': ['pip>=6.0,<6.1']}), + ('pip-latest', {'modules': ['pip']}), + ('setuptools-EL7', {'modules': ['pip==1.4.1', 'setuptools==0.9.8']}), + ('setuptools-Trusty', {'modules': ['pip==1.5', 'setuptools==2.2']}), + ('setuptools-minimum', {'modules': ['pip==1.5', 'setuptools==0.7.2']}), + ] + + @testtools.skipUnless( + os.environ.get('PBR_INTEGRATION', None) == '1', + 'integration tests not enabled') + def test_pip_versions(self): + pkgs = { + 'test_markers': + {'requirements.txt': textwrap.dedent("""\ + pkg_a; python_version=='1.2' + pkg_b; python_version!='1.2' + """)}, + 'pkg_a': {}, + 'pkg_b': {}, + } + pkg_dirs = self.useFixture( + test_packaging.CreatePackages(pkgs)).package_dirs + temp_dir = self.useFixture(fixtures.TempDir()).path + repo_dir = os.path.join(temp_dir, 'repo') + venv = self.useFixture(test_packaging.Venv('markers')) + bin_python = venv.python + os.mkdir(repo_dir) + for module in self.modules: + self._run_cmd( + bin_python, + ['-m', 'pip', 'install', '--upgrade', module], + cwd=venv.path, allow_fail=False) + for pkg in pkg_dirs: + self._run_cmd( + bin_python, ['setup.py', 'sdist', '-d', repo_dir], + cwd=pkg_dirs[pkg], allow_fail=False) + self._run_cmd( + bin_python, + ['-m', 'pip', 'install', '--no-index', '-f', repo_dir, + 'test_markers'], + cwd=venv.path, allow_fail=False) + self.assertIn('pkg-b', self._run_cmd( + bin_python, ['-m', 'pip', 'freeze'], cwd=venv.path, + allow_fail=False)[0]) + + +class TestLTSSupport(base.BaseTestCase): + + # These versions come from the versions installed from the 'virtualenv' + # command from the 'python-virtualenv' package. + scenarios = [ + ('EL7', {'modules': ['pip==1.4.1', 'setuptools==0.9.8'], + 'py3support': True}), # And EPEL6 + ('Trusty', {'modules': ['pip==1.5', 'setuptools==2.2'], + 'py3support': True}), + ('Jessie', {'modules': ['pip==1.5.6', 'setuptools==5.5.1'], + 'py3support': True}), + # Wheezy has pip1.1, which cannot be called with '-m pip' + # So we'll use a different version of pip here. + ('WheezyPrecise', {'modules': ['pip==1.4.1', 'setuptools==0.6c11'], + 'py3support': False}) + ] + + @testtools.skipUnless( + os.environ.get('PBR_INTEGRATION', None) == '1', + 'integration tests not enabled') + def test_lts_venv_default_versions(self): + if (sys.version_info[0] == 3 and not self.py3support): + self.skipTest('This combination will not install with py3, ' + 'skipping test') + venv = self.useFixture( + test_packaging.Venv('setuptools', modules=self.modules)) + bin_python = venv.python + pbr = 'file://%s#egg=pbr' % PBR_ROOT + # Installing PBR is a reasonable indication that we are not broken on + # this particular combination of setuptools and pip. + self._run_cmd(bin_python, ['-m', 'pip', 'install', pbr], + cwd=venv.path, allow_fail=False) diff --git a/libs/pbr/tests/test_packaging.py b/libs/pbr/tests/test_packaging.py new file mode 100644 index 00000000..d19dd05b --- /dev/null +++ b/libs/pbr/tests/test_packaging.py @@ -0,0 +1,923 @@ +# Copyright (c) 2013 New Dream Network, LLC (DreamHost) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Copyright (C) 2013 Association of Universities for Research in Astronomy +# (AURA) +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# +# 3. The name of AURA and its representatives may not be used to +# endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR IMPLIED +# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + +import email +import email.errors +import imp +import os +import re +import sysconfig +import tempfile +import textwrap + +import fixtures +import mock +import pkg_resources +import six +import testscenarios +import testtools +from testtools import matchers +import virtualenv +from wheel import wheelfile + +from pbr import git +from pbr import packaging +from pbr.tests import base + + +PBR_ROOT = os.path.abspath(os.path.join(__file__, '..', '..', '..')) + + +class TestRepo(fixtures.Fixture): + """A git repo for testing with. + + Use of TempHomeDir with this fixture is strongly recommended as due to the + lack of config --local in older gits, it will write to the users global + configuration without TempHomeDir. + """ + + def __init__(self, basedir): + super(TestRepo, self).__init__() + self._basedir = basedir + + def setUp(self): + super(TestRepo, self).setUp() + base._run_cmd(['git', 'init', '.'], self._basedir) + base._config_git() + base._run_cmd(['git', 'add', '.'], self._basedir) + + def commit(self, message_content='test commit'): + files = len(os.listdir(self._basedir)) + path = self._basedir + '/%d' % files + open(path, 'wt').close() + base._run_cmd(['git', 'add', path], self._basedir) + base._run_cmd(['git', 'commit', '-m', message_content], self._basedir) + + def uncommit(self): + base._run_cmd(['git', 'reset', '--hard', 'HEAD^'], self._basedir) + + def tag(self, version): + base._run_cmd( + ['git', 'tag', '-sm', 'test tag', version], self._basedir) + + +class GPGKeyFixture(fixtures.Fixture): + """Creates a GPG key for testing. + + It's recommended that this be used in concert with a unique home + directory. + """ + + def setUp(self): + super(GPGKeyFixture, self).setUp() + tempdir = self.useFixture(fixtures.TempDir()) + gnupg_version_re = re.compile('^gpg\s.*\s([\d+])\.([\d+])\.([\d+])') + gnupg_version = base._run_cmd(['gpg', '--version'], tempdir.path) + for line in gnupg_version[0].split('\n'): + gnupg_version = gnupg_version_re.match(line) + if gnupg_version: + gnupg_version = (int(gnupg_version.group(1)), + int(gnupg_version.group(2)), + int(gnupg_version.group(3))) + break + else: + if gnupg_version is None: + gnupg_version = (0, 0, 0) + config_file = tempdir.path + '/key-config' + f = open(config_file, 'wt') + try: + if gnupg_version[0] == 2 and gnupg_version[1] >= 1: + f.write(""" + %no-protection + %transient-key + """) + f.write(""" + %no-ask-passphrase + Key-Type: RSA + Name-Real: Example Key + Name-Comment: N/A + Name-Email: example@example.com + Expire-Date: 2d + Preferences: (setpref) + %commit + """) + finally: + f.close() + # Note that --quick-random (--debug-quick-random in GnuPG 2.x) + # does not have a corresponding preferences file setting and + # must be passed explicitly on the command line instead + if gnupg_version[0] == 1: + gnupg_random = '--quick-random' + elif gnupg_version[0] >= 2: + gnupg_random = '--debug-quick-random' + else: + gnupg_random = '' + base._run_cmd( + ['gpg', '--gen-key', '--batch', gnupg_random, config_file], + tempdir.path) + + +class Venv(fixtures.Fixture): + """Create a virtual environment for testing with. + + :attr path: The path to the environment root. + :attr python: The path to the python binary in the environment. + """ + + def __init__(self, reason, modules=(), pip_cmd=None): + """Create a Venv fixture. + + :param reason: A human readable string to bake into the venv + file path to aid diagnostics in the case of failures. + :param modules: A list of modules to install, defaults to latest + pip, wheel, and the working copy of PBR. + :attr pip_cmd: A list to override the default pip_cmd passed to + python for installing base packages. + """ + self._reason = reason + if modules == (): + pbr = 'file://%s#egg=pbr' % PBR_ROOT + modules = ['pip', 'wheel', pbr] + self.modules = modules + if pip_cmd is None: + self.pip_cmd = ['-m', 'pip', 'install'] + else: + self.pip_cmd = pip_cmd + + def _setUp(self): + path = self.useFixture(fixtures.TempDir()).path + virtualenv.create_environment(path, clear=True) + python = os.path.join(path, 'bin', 'python') + command = [python] + self.pip_cmd + ['-U'] + if self.modules and len(self.modules) > 0: + command.extend(self.modules) + self.useFixture(base.CapturedSubprocess( + 'mkvenv-' + self._reason, command)) + self.addCleanup(delattr, self, 'path') + self.addCleanup(delattr, self, 'python') + self.path = path + self.python = python + return path, python + + +class CreatePackages(fixtures.Fixture): + """Creates packages from dict with defaults + + :param package_dirs: A dict of package name to directory strings + {'pkg_a': '/tmp/path/to/tmp/pkg_a', 'pkg_b': '/tmp/path/to/tmp/pkg_b'} + """ + + defaults = { + 'setup.py': textwrap.dedent(six.u("""\ + #!/usr/bin/env python + import setuptools + setuptools.setup( + setup_requires=['pbr'], + pbr=True, + ) + """)), + 'setup.cfg': textwrap.dedent(six.u("""\ + [metadata] + name = {pkg_name} + """)) + } + + def __init__(self, packages): + """Creates packages from dict with defaults + + :param packages: a dict where the keys are the package name and a + value that is a second dict that may be empty, containing keys of + filenames and a string value of the contents. + {'package-a': {'requirements.txt': 'string', 'setup.cfg': 'string'} + """ + self.packages = packages + + def _writeFile(self, directory, file_name, contents): + path = os.path.abspath(os.path.join(directory, file_name)) + path_dir = os.path.dirname(path) + if not os.path.exists(path_dir): + if path_dir.startswith(directory): + os.makedirs(path_dir) + else: + raise ValueError + with open(path, 'wt') as f: + f.write(contents) + + def _setUp(self): + tmpdir = self.useFixture(fixtures.TempDir()).path + package_dirs = {} + for pkg_name in self.packages: + pkg_path = os.path.join(tmpdir, pkg_name) + package_dirs[pkg_name] = pkg_path + os.mkdir(pkg_path) + for cf in ['setup.py', 'setup.cfg']: + if cf in self.packages[pkg_name]: + contents = self.packages[pkg_name].pop(cf) + else: + contents = self.defaults[cf].format(pkg_name=pkg_name) + self._writeFile(pkg_path, cf, contents) + + for cf in self.packages[pkg_name]: + self._writeFile(pkg_path, cf, self.packages[pkg_name][cf]) + self.useFixture(TestRepo(pkg_path)).commit() + self.addCleanup(delattr, self, 'package_dirs') + self.package_dirs = package_dirs + return package_dirs + + +class TestPackagingInGitRepoWithCommit(base.BaseTestCase): + + scenarios = [ + ('preversioned', dict(preversioned=True)), + ('postversioned', dict(preversioned=False)), + ] + + def setUp(self): + super(TestPackagingInGitRepoWithCommit, self).setUp() + self.repo = self.useFixture(TestRepo(self.package_dir)) + self.repo.commit() + + def test_authors(self): + self.run_setup('sdist', allow_fail=False) + # One commit, something should be in the authors list + with open(os.path.join(self.package_dir, 'AUTHORS'), 'r') as f: + body = f.read() + self.assertNotEqual(body, '') + + def test_changelog(self): + self.run_setup('sdist', allow_fail=False) + with open(os.path.join(self.package_dir, 'ChangeLog'), 'r') as f: + body = f.read() + # One commit, something should be in the ChangeLog list + self.assertNotEqual(body, '') + + def test_changelog_handles_astrisk(self): + self.repo.commit(message_content="Allow *.openstack.org to work") + self.run_setup('sdist', allow_fail=False) + with open(os.path.join(self.package_dir, 'ChangeLog'), 'r') as f: + body = f.read() + self.assertIn('\*', body) + + def test_changelog_handles_dead_links_in_commit(self): + self.repo.commit(message_content="See os_ for to_do about qemu_.") + self.run_setup('sdist', allow_fail=False) + with open(os.path.join(self.package_dir, 'ChangeLog'), 'r') as f: + body = f.read() + self.assertIn('os\_', body) + self.assertIn('to\_do', body) + self.assertIn('qemu\_', body) + + def test_changelog_handles_backticks(self): + self.repo.commit(message_content="Allow `openstack.org` to `work") + self.run_setup('sdist', allow_fail=False) + with open(os.path.join(self.package_dir, 'ChangeLog'), 'r') as f: + body = f.read() + self.assertIn('\`', body) + + def test_manifest_exclude_honoured(self): + self.run_setup('sdist', allow_fail=False) + with open(os.path.join( + self.package_dir, + 'pbr_testpackage.egg-info/SOURCES.txt'), 'r') as f: + body = f.read() + self.assertThat( + body, matchers.Not(matchers.Contains('pbr_testpackage/extra.py'))) + self.assertThat(body, matchers.Contains('pbr_testpackage/__init__.py')) + + def test_install_writes_changelog(self): + stdout, _, _ = self.run_setup( + 'install', '--root', self.temp_dir + 'installed', + allow_fail=False) + self.expectThat(stdout, matchers.Contains('Generating ChangeLog')) + + +class TestExtrafileInstallation(base.BaseTestCase): + def test_install_glob(self): + stdout, _, _ = self.run_setup( + 'install', '--root', self.temp_dir + 'installed', + allow_fail=False) + self.expectThat( + stdout, matchers.Contains('copying data_files/a.txt')) + self.expectThat( + stdout, matchers.Contains('copying data_files/b.txt')) + + +class TestPackagingInGitRepoWithoutCommit(base.BaseTestCase): + + def setUp(self): + super(TestPackagingInGitRepoWithoutCommit, self).setUp() + self.useFixture(TestRepo(self.package_dir)) + self.run_setup('sdist', allow_fail=False) + + def test_authors(self): + # No commits, no authors in list + with open(os.path.join(self.package_dir, 'AUTHORS'), 'r') as f: + body = f.read() + self.assertEqual('\n', body) + + def test_changelog(self): + # No commits, nothing should be in the ChangeLog list + with open(os.path.join(self.package_dir, 'ChangeLog'), 'r') as f: + body = f.read() + self.assertEqual('CHANGES\n=======\n\n', body) + + +class TestPackagingWheels(base.BaseTestCase): + + def setUp(self): + super(TestPackagingWheels, self).setUp() + self.useFixture(TestRepo(self.package_dir)) + # Build the wheel + self.run_setup('bdist_wheel', allow_fail=False) + # Slowly construct the path to the generated whl + dist_dir = os.path.join(self.package_dir, 'dist') + relative_wheel_filename = os.listdir(dist_dir)[0] + absolute_wheel_filename = os.path.join( + dist_dir, relative_wheel_filename) + wheel_file = wheelfile.WheelFile(absolute_wheel_filename) + wheel_name = wheel_file.parsed_filename.group('namever') + # Create a directory path to unpack the wheel to + self.extracted_wheel_dir = os.path.join(dist_dir, wheel_name) + # Extract the wheel contents to the directory we just created + wheel_file.extractall(self.extracted_wheel_dir) + wheel_file.close() + + def test_data_directory_has_wsgi_scripts(self): + # Build the path to the scripts directory + scripts_dir = os.path.join( + self.extracted_wheel_dir, 'pbr_testpackage-0.0.data/scripts') + self.assertTrue(os.path.exists(scripts_dir)) + scripts = os.listdir(scripts_dir) + + self.assertIn('pbr_test_wsgi', scripts) + self.assertIn('pbr_test_wsgi_with_class', scripts) + self.assertNotIn('pbr_test_cmd', scripts) + self.assertNotIn('pbr_test_cmd_with_class', scripts) + + def test_generates_c_extensions(self): + built_package_dir = os.path.join( + self.extracted_wheel_dir, 'pbr_testpackage') + static_object_filename = 'testext.so' + soabi = get_soabi() + if soabi: + static_object_filename = 'testext.{0}.so'.format(soabi) + static_object_path = os.path.join( + built_package_dir, static_object_filename) + + self.assertTrue(os.path.exists(built_package_dir)) + self.assertTrue(os.path.exists(static_object_path)) + + +class TestPackagingHelpers(testtools.TestCase): + + def test_generate_script(self): + group = 'console_scripts' + entry_point = pkg_resources.EntryPoint( + name='test-ep', + module_name='pbr.packaging', + attrs=('LocalInstallScripts',)) + header = '#!/usr/bin/env fake-header\n' + template = ('%(group)s %(module_name)s %(import_target)s ' + '%(invoke_target)s') + + generated_script = packaging.generate_script( + group, entry_point, header, template) + + expected_script = ( + '#!/usr/bin/env fake-header\nconsole_scripts pbr.packaging ' + 'LocalInstallScripts LocalInstallScripts' + ) + self.assertEqual(expected_script, generated_script) + + def test_generate_script_validates_expectations(self): + group = 'console_scripts' + entry_point = pkg_resources.EntryPoint( + name='test-ep', + module_name='pbr.packaging') + header = '#!/usr/bin/env fake-header\n' + template = ('%(group)s %(module_name)s %(import_target)s ' + '%(invoke_target)s') + self.assertRaises( + ValueError, packaging.generate_script, group, entry_point, header, + template) + + entry_point = pkg_resources.EntryPoint( + name='test-ep', + module_name='pbr.packaging', + attrs=('attr1', 'attr2', 'attr3')) + self.assertRaises( + ValueError, packaging.generate_script, group, entry_point, header, + template) + + +class TestPackagingInPlainDirectory(base.BaseTestCase): + + def setUp(self): + super(TestPackagingInPlainDirectory, self).setUp() + + def test_authors(self): + self.run_setup('sdist', allow_fail=False) + # Not a git repo, no AUTHORS file created + filename = os.path.join(self.package_dir, 'AUTHORS') + self.assertFalse(os.path.exists(filename)) + + def test_changelog(self): + self.run_setup('sdist', allow_fail=False) + # Not a git repo, no ChangeLog created + filename = os.path.join(self.package_dir, 'ChangeLog') + self.assertFalse(os.path.exists(filename)) + + def test_install_no_ChangeLog(self): + stdout, _, _ = self.run_setup( + 'install', '--root', self.temp_dir + 'installed', + allow_fail=False) + self.expectThat( + stdout, matchers.Not(matchers.Contains('Generating ChangeLog'))) + + +class TestPresenceOfGit(base.BaseTestCase): + + def testGitIsInstalled(self): + with mock.patch.object(git, + '_run_shell_command') as _command: + _command.return_value = 'git version 1.8.4.1' + self.assertEqual(True, git._git_is_installed()) + + def testGitIsNotInstalled(self): + with mock.patch.object(git, + '_run_shell_command') as _command: + _command.side_effect = OSError + self.assertEqual(False, git._git_is_installed()) + + +class ParseRequirementsTest(base.BaseTestCase): + + def test_empty_requirements(self): + actual = packaging.parse_requirements([]) + self.assertEqual([], actual) + + def test_default_requirements(self): + """Ensure default files used if no files provided.""" + tempdir = tempfile.mkdtemp() + requirements = os.path.join(tempdir, 'requirements.txt') + with open(requirements, 'w') as f: + f.write('pbr') + # the defaults are relative to where pbr is called from so we need to + # override them. This is OK, however, as we want to validate that + # defaults are used - not what those defaults are + with mock.patch.object(packaging, 'REQUIREMENTS_FILES', ( + requirements,)): + result = packaging.parse_requirements() + self.assertEqual(['pbr'], result) + + def test_override_with_env(self): + """Ensure environment variable used if no files provided.""" + _, tmp_file = tempfile.mkstemp(prefix='openstack', suffix='.setup') + with open(tmp_file, 'w') as fh: + fh.write("foo\nbar") + self.useFixture( + fixtures.EnvironmentVariable('PBR_REQUIREMENTS_FILES', tmp_file)) + self.assertEqual(['foo', 'bar'], + packaging.parse_requirements()) + + def test_override_with_env_multiple_files(self): + _, tmp_file = tempfile.mkstemp(prefix='openstack', suffix='.setup') + with open(tmp_file, 'w') as fh: + fh.write("foo\nbar") + self.useFixture( + fixtures.EnvironmentVariable('PBR_REQUIREMENTS_FILES', + "no-such-file," + tmp_file)) + self.assertEqual(['foo', 'bar'], + packaging.parse_requirements()) + + def test_index_present(self): + tempdir = tempfile.mkdtemp() + requirements = os.path.join(tempdir, 'requirements.txt') + with open(requirements, 'w') as f: + f.write('-i https://myindex.local') + f.write(' --index-url https://myindex.local') + f.write(' --extra-index-url https://myindex.local') + result = packaging.parse_requirements([requirements]) + self.assertEqual([], result) + + def test_nested_requirements(self): + tempdir = tempfile.mkdtemp() + requirements = os.path.join(tempdir, 'requirements.txt') + nested = os.path.join(tempdir, 'nested.txt') + with open(requirements, 'w') as f: + f.write('-r ' + nested) + with open(nested, 'w') as f: + f.write('pbr') + result = packaging.parse_requirements([requirements]) + self.assertEqual(['pbr'], result) + + +class ParseRequirementsTestScenarios(base.BaseTestCase): + + versioned_scenarios = [ + ('non-versioned', {'versioned': False, 'expected': ['bar']}), + ('versioned', {'versioned': True, 'expected': ['bar>=1.2.3']}) + ] + + subdirectory_scenarios = [ + ('non-subdirectory', {'has_subdirectory': False}), + ('has-subdirectory', {'has_subdirectory': True}) + ] + + scenarios = [ + ('normal', {'url': "foo\nbar", 'expected': ['foo', 'bar']}), + ('normal_with_comments', { + 'url': "# this is a comment\nfoo\n# and another one\nbar", + 'expected': ['foo', 'bar']}), + ('removes_index_lines', {'url': '-f foobar', 'expected': []}), + ] + + scenarios = scenarios + testscenarios.multiply_scenarios([ + ('ssh_egg_url', {'url': 'git+ssh://foo.com/zipball#egg=bar'}), + ('git_https_egg_url', {'url': 'git+https://foo.com/zipball#egg=bar'}), + ('http_egg_url', {'url': 'https://foo.com/zipball#egg=bar'}), + ], versioned_scenarios, subdirectory_scenarios) + + scenarios = scenarios + testscenarios.multiply_scenarios( + [ + ('git_egg_url', + {'url': 'git://foo.com/zipball#egg=bar', 'name': 'bar'}) + ], [ + ('non-editable', {'editable': False}), + ('editable', {'editable': True}), + ], + versioned_scenarios, subdirectory_scenarios) + + def test_parse_requirements(self): + tmp_file = tempfile.NamedTemporaryFile() + req_string = self.url + if hasattr(self, 'editable') and self.editable: + req_string = ("-e %s" % req_string) + if hasattr(self, 'versioned') and self.versioned: + req_string = ("%s-1.2.3" % req_string) + if hasattr(self, 'has_subdirectory') and self.has_subdirectory: + req_string = ("%s&subdirectory=baz" % req_string) + with open(tmp_file.name, 'w') as fh: + fh.write(req_string) + self.assertEqual(self.expected, + packaging.parse_requirements([tmp_file.name])) + + +class ParseDependencyLinksTest(base.BaseTestCase): + + def setUp(self): + super(ParseDependencyLinksTest, self).setUp() + _, self.tmp_file = tempfile.mkstemp(prefix="openstack", + suffix=".setup") + + def test_parse_dependency_normal(self): + with open(self.tmp_file, "w") as fh: + fh.write("http://test.com\n") + self.assertEqual( + ["http://test.com"], + packaging.parse_dependency_links([self.tmp_file])) + + def test_parse_dependency_with_git_egg_url(self): + with open(self.tmp_file, "w") as fh: + fh.write("-e git://foo.com/zipball#egg=bar") + self.assertEqual( + ["git://foo.com/zipball#egg=bar"], + packaging.parse_dependency_links([self.tmp_file])) + + +class TestVersions(base.BaseTestCase): + + scenarios = [ + ('preversioned', dict(preversioned=True)), + ('postversioned', dict(preversioned=False)), + ] + + def setUp(self): + super(TestVersions, self).setUp() + self.repo = self.useFixture(TestRepo(self.package_dir)) + self.useFixture(GPGKeyFixture()) + self.useFixture(base.DiveDir(self.package_dir)) + + def test_email_parsing_errors_are_handled(self): + mocked_open = mock.mock_open() + with mock.patch('pbr.packaging.open', mocked_open): + with mock.patch('email.message_from_file') as message_from_file: + message_from_file.side_effect = [ + email.errors.MessageError('Test'), + {'Name': 'pbr_testpackage'}] + version = packaging._get_version_from_pkg_metadata( + 'pbr_testpackage') + + self.assertTrue(message_from_file.called) + self.assertIsNone(version) + + def test_capitalized_headers(self): + self.repo.commit() + self.repo.tag('1.2.3') + self.repo.commit('Sem-Ver: api-break') + version = packaging._get_version_from_git() + self.assertThat(version, matchers.StartsWith('2.0.0.dev1')) + + def test_capitalized_headers_partial(self): + self.repo.commit() + self.repo.tag('1.2.3') + self.repo.commit('Sem-ver: api-break') + version = packaging._get_version_from_git() + self.assertThat(version, matchers.StartsWith('2.0.0.dev1')) + + def test_tagged_version_has_tag_version(self): + self.repo.commit() + self.repo.tag('1.2.3') + version = packaging._get_version_from_git('1.2.3') + self.assertEqual('1.2.3', version) + + def test_non_canonical_tagged_version_bump(self): + self.repo.commit() + self.repo.tag('1.4') + self.repo.commit('Sem-Ver: api-break') + version = packaging._get_version_from_git() + self.assertThat(version, matchers.StartsWith('2.0.0.dev1')) + + def test_untagged_version_has_dev_version_postversion(self): + self.repo.commit() + self.repo.tag('1.2.3') + self.repo.commit() + version = packaging._get_version_from_git() + self.assertThat(version, matchers.StartsWith('1.2.4.dev1')) + + def test_untagged_pre_release_has_pre_dev_version_postversion(self): + self.repo.commit() + self.repo.tag('1.2.3.0a1') + self.repo.commit() + version = packaging._get_version_from_git() + self.assertThat(version, matchers.StartsWith('1.2.3.0a2.dev1')) + + def test_untagged_version_minor_bump(self): + self.repo.commit() + self.repo.tag('1.2.3') + self.repo.commit('sem-ver: deprecation') + version = packaging._get_version_from_git() + self.assertThat(version, matchers.StartsWith('1.3.0.dev1')) + + def test_untagged_version_major_bump(self): + self.repo.commit() + self.repo.tag('1.2.3') + self.repo.commit('sem-ver: api-break') + version = packaging._get_version_from_git() + self.assertThat(version, matchers.StartsWith('2.0.0.dev1')) + + def test_untagged_version_has_dev_version_preversion(self): + self.repo.commit() + self.repo.tag('1.2.3') + self.repo.commit() + version = packaging._get_version_from_git('1.2.5') + self.assertThat(version, matchers.StartsWith('1.2.5.dev1')) + + def test_untagged_version_after_pre_has_dev_version_preversion(self): + self.repo.commit() + self.repo.tag('1.2.3.0a1') + self.repo.commit() + version = packaging._get_version_from_git('1.2.5') + self.assertThat(version, matchers.StartsWith('1.2.5.dev1')) + + def test_untagged_version_after_rc_has_dev_version_preversion(self): + self.repo.commit() + self.repo.tag('1.2.3.0a1') + self.repo.commit() + version = packaging._get_version_from_git('1.2.3') + self.assertThat(version, matchers.StartsWith('1.2.3.0a2.dev1')) + + def test_preversion_too_low_simple(self): + # That is, the target version is either already released or not high + # enough for the semver requirements given api breaks etc. + self.repo.commit() + self.repo.tag('1.2.3') + self.repo.commit() + # Note that we can't target 1.2.3 anymore - with 1.2.3 released we + # need to be working on 1.2.4. + err = self.assertRaises( + ValueError, packaging._get_version_from_git, '1.2.3') + self.assertThat(err.args[0], matchers.StartsWith('git history')) + + def test_preversion_too_low_semver_headers(self): + # That is, the target version is either already released or not high + # enough for the semver requirements given api breaks etc. + self.repo.commit() + self.repo.tag('1.2.3') + self.repo.commit('sem-ver: feature') + # Note that we can't target 1.2.4, the feature header means we need + # to be working on 1.3.0 or above. + err = self.assertRaises( + ValueError, packaging._get_version_from_git, '1.2.4') + self.assertThat(err.args[0], matchers.StartsWith('git history')) + + def test_get_kwargs_corner_cases(self): + # No tags: + git_dir = self.repo._basedir + '/.git' + get_kwargs = lambda tag: packaging._get_increment_kwargs(git_dir, tag) + + def _check_combinations(tag): + self.repo.commit() + self.assertEqual(dict(), get_kwargs(tag)) + self.repo.commit('sem-ver: bugfix') + self.assertEqual(dict(), get_kwargs(tag)) + self.repo.commit('sem-ver: feature') + self.assertEqual(dict(minor=True), get_kwargs(tag)) + self.repo.uncommit() + self.repo.commit('sem-ver: deprecation') + self.assertEqual(dict(minor=True), get_kwargs(tag)) + self.repo.uncommit() + self.repo.commit('sem-ver: api-break') + self.assertEqual(dict(major=True), get_kwargs(tag)) + self.repo.commit('sem-ver: deprecation') + self.assertEqual(dict(major=True, minor=True), get_kwargs(tag)) + _check_combinations('') + self.repo.tag('1.2.3') + _check_combinations('1.2.3') + + def test_invalid_tag_ignored(self): + # Fix for bug 1356784 - we treated any tag as a version, not just those + # that are valid versions. + self.repo.commit() + self.repo.tag('1') + self.repo.commit() + # when the tree is tagged and its wrong: + self.repo.tag('badver') + version = packaging._get_version_from_git() + self.assertThat(version, matchers.StartsWith('1.0.1.dev1')) + # When the tree isn't tagged, we also fall through. + self.repo.commit() + version = packaging._get_version_from_git() + self.assertThat(version, matchers.StartsWith('1.0.1.dev2')) + # We don't fall through x.y versions + self.repo.commit() + self.repo.tag('1.2') + self.repo.commit() + self.repo.tag('badver2') + version = packaging._get_version_from_git() + self.assertThat(version, matchers.StartsWith('1.2.1.dev1')) + # Or x.y.z versions + self.repo.commit() + self.repo.tag('1.2.3') + self.repo.commit() + self.repo.tag('badver3') + version = packaging._get_version_from_git() + self.assertThat(version, matchers.StartsWith('1.2.4.dev1')) + # Or alpha/beta/pre versions + self.repo.commit() + self.repo.tag('1.2.4.0a1') + self.repo.commit() + self.repo.tag('badver4') + version = packaging._get_version_from_git() + self.assertThat(version, matchers.StartsWith('1.2.4.0a2.dev1')) + # Non-release related tags are ignored. + self.repo.commit() + self.repo.tag('2') + self.repo.commit() + self.repo.tag('non-release-tag/2014.12.16-1') + version = packaging._get_version_from_git() + self.assertThat(version, matchers.StartsWith('2.0.1.dev1')) + + def test_valid_tag_honoured(self): + # Fix for bug 1370608 - we converted any target into a 'dev version' + # even if there was a distance of 0 - indicating that we were on the + # tag itself. + self.repo.commit() + self.repo.tag('1.3.0.0a1') + version = packaging._get_version_from_git() + self.assertEqual('1.3.0.0a1', version) + + def test_skip_write_git_changelog(self): + # Fix for bug 1467440 + self.repo.commit() + self.repo.tag('1.2.3') + os.environ['SKIP_WRITE_GIT_CHANGELOG'] = '1' + version = packaging._get_version_from_git('1.2.3') + self.assertEqual('1.2.3', version) + + def tearDown(self): + super(TestVersions, self).tearDown() + os.environ.pop('SKIP_WRITE_GIT_CHANGELOG', None) + + +class TestRequirementParsing(base.BaseTestCase): + + def test_requirement_parsing(self): + pkgs = { + 'test_reqparse': + { + 'requirements.txt': textwrap.dedent("""\ + bar + quux<1.0; python_version=='2.6' + requests-aws>=0.1.4 # BSD License (3 clause) + Routes>=1.12.3,!=2.0,!=2.1;python_version=='2.7' + requests-kerberos>=0.6;python_version=='2.7' # MIT + """), + 'setup.cfg': textwrap.dedent("""\ + [metadata] + name = test_reqparse + + [extras] + test = + foo + baz>3.2 :python_version=='2.7' # MIT + bar>3.3 :python_version=='2.7' # MIT # Apache + """)}, + } + pkg_dirs = self.useFixture(CreatePackages(pkgs)).package_dirs + pkg_dir = pkg_dirs['test_reqparse'] + # pkg_resources.split_sections uses None as the title of an + # anonymous section instead of the empty string. Weird. + expected_requirements = { + None: ['bar', 'requests-aws>=0.1.4'], + ":(python_version=='2.6')": ['quux<1.0'], + ":(python_version=='2.7')": ['Routes!=2.0,!=2.1,>=1.12.3', + 'requests-kerberos>=0.6'], + 'test': ['foo'], + "test:(python_version=='2.7')": ['baz>3.2', 'bar>3.3'] + } + venv = self.useFixture(Venv('reqParse')) + bin_python = venv.python + # Two things are tested by this + # 1) pbr properly parses markers from requiremnts.txt and setup.cfg + # 2) bdist_wheel causes pbr to not evaluate markers + self._run_cmd(bin_python, ('setup.py', 'bdist_wheel'), + allow_fail=False, cwd=pkg_dir) + egg_info = os.path.join(pkg_dir, 'test_reqparse.egg-info') + + requires_txt = os.path.join(egg_info, 'requires.txt') + with open(requires_txt, 'rt') as requires: + generated_requirements = dict( + pkg_resources.split_sections(requires)) + + # NOTE(dhellmann): We have to spell out the comparison because + # the rendering for version specifiers in a range is not + # consistent across versions of setuptools. + + for section, expected in expected_requirements.items(): + exp_parsed = [ + pkg_resources.Requirement.parse(s) + for s in expected + ] + gen_parsed = [ + pkg_resources.Requirement.parse(s) + for s in generated_requirements[section] + ] + self.assertEqual(exp_parsed, gen_parsed) + + +def get_soabi(): + soabi = None + try: + soabi = sysconfig.get_config_var('SOABI') + arch = sysconfig.get_config_var('MULTIARCH') + except IOError: + pass + if soabi and arch and 'pypy' in sysconfig.get_scheme_names(): + soabi = '%s-%s' % (soabi, arch) + if soabi is None and 'pypy' in sysconfig.get_scheme_names(): + # NOTE(sigmavirus24): PyPy only added support for the SOABI config var + # to sysconfig in 2015. That was well after 2.2.1 was published in the + # Ubuntu 14.04 archive. + for suffix, _, _ in imp.get_suffixes(): + if suffix.startswith('.pypy') and suffix.endswith('.so'): + soabi = suffix.split('.')[1] + break + return soabi diff --git a/libs/pbr/tests/test_pbr_json.py b/libs/pbr/tests/test_pbr_json.py new file mode 100644 index 00000000..f0669713 --- /dev/null +++ b/libs/pbr/tests/test_pbr_json.py @@ -0,0 +1,30 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock + +from pbr import pbr_json +from pbr.tests import base + + +class TestJsonContent(base.BaseTestCase): + @mock.patch('pbr.git._run_git_functions', return_value=True) + @mock.patch('pbr.git.get_git_short_sha', return_value="123456") + @mock.patch('pbr.git.get_is_release', return_value=True) + def test_content(self, mock_get_is, mock_get_git, mock_run): + cmd = mock.Mock() + pbr_json.write_pbr_json(cmd, "basename", "pbr.json") + cmd.write_file.assert_called_once_with( + 'pbr', + 'pbr.json', + '{"git_version": "123456", "is_release": true}' + ) diff --git a/libs/pbr/tests/test_setup.py b/libs/pbr/tests/test_setup.py new file mode 100644 index 00000000..85d40ebf --- /dev/null +++ b/libs/pbr/tests/test_setup.py @@ -0,0 +1,445 @@ +# Copyright (c) 2011 OpenStack Foundation +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import print_function + +import os + +try: + import cStringIO as io + BytesIO = io.StringIO +except ImportError: + import io + BytesIO = io.BytesIO + +import fixtures + +from pbr import git +from pbr import options +from pbr import packaging +from pbr.tests import base + + +class SkipFileWrites(base.BaseTestCase): + + scenarios = [ + ('changelog_option_true', + dict(option_key='skip_changelog', option_value='True', + env_key='SKIP_WRITE_GIT_CHANGELOG', env_value=None, + pkg_func=git.write_git_changelog, filename='ChangeLog')), + ('changelog_option_false', + dict(option_key='skip_changelog', option_value='False', + env_key='SKIP_WRITE_GIT_CHANGELOG', env_value=None, + pkg_func=git.write_git_changelog, filename='ChangeLog')), + ('changelog_env_true', + dict(option_key='skip_changelog', option_value='False', + env_key='SKIP_WRITE_GIT_CHANGELOG', env_value='True', + pkg_func=git.write_git_changelog, filename='ChangeLog')), + ('changelog_both_true', + dict(option_key='skip_changelog', option_value='True', + env_key='SKIP_WRITE_GIT_CHANGELOG', env_value='True', + pkg_func=git.write_git_changelog, filename='ChangeLog')), + ('authors_option_true', + dict(option_key='skip_authors', option_value='True', + env_key='SKIP_GENERATE_AUTHORS', env_value=None, + pkg_func=git.generate_authors, filename='AUTHORS')), + ('authors_option_false', + dict(option_key='skip_authors', option_value='False', + env_key='SKIP_GENERATE_AUTHORS', env_value=None, + pkg_func=git.generate_authors, filename='AUTHORS')), + ('authors_env_true', + dict(option_key='skip_authors', option_value='False', + env_key='SKIP_GENERATE_AUTHORS', env_value='True', + pkg_func=git.generate_authors, filename='AUTHORS')), + ('authors_both_true', + dict(option_key='skip_authors', option_value='True', + env_key='SKIP_GENERATE_AUTHORS', env_value='True', + pkg_func=git.generate_authors, filename='AUTHORS')), + ] + + def setUp(self): + super(SkipFileWrites, self).setUp() + self.temp_path = self.useFixture(fixtures.TempDir()).path + self.root_dir = os.path.abspath(os.path.curdir) + self.git_dir = os.path.join(self.root_dir, ".git") + if not os.path.exists(self.git_dir): + self.skipTest("%s is missing; skipping git-related checks" + % self.git_dir) + return + self.filename = os.path.join(self.temp_path, self.filename) + self.option_dict = dict() + if self.option_key is not None: + self.option_dict[self.option_key] = ('setup.cfg', + self.option_value) + self.useFixture( + fixtures.EnvironmentVariable(self.env_key, self.env_value)) + + def test_skip(self): + self.pkg_func(git_dir=self.git_dir, + dest_dir=self.temp_path, + option_dict=self.option_dict) + self.assertEqual( + not os.path.exists(self.filename), + (self.option_value.lower() in options.TRUE_VALUES + or self.env_value is not None)) + +_changelog_content = """7780758\x00Break parser\x00 (tag: refs/tags/1_foo.1) +04316fe\x00Make python\x00 (refs/heads/review/monty_taylor/27519) +378261a\x00Add an integration test script.\x00 +3c373ac\x00Merge "Lib\x00 (HEAD, tag: refs/tags/2013.2.rc2, tag: refs/tags/2013.2, refs/heads/mile-proposed) +182feb3\x00Fix pip invocation for old versions of pip.\x00 (tag: refs/tags/0.5.17) +fa4f46e\x00Remove explicit depend on distribute.\x00 (tag: refs/tags/0.5.16) +d1c53dd\x00Use pip instead of easy_install for installation.\x00 +a793ea1\x00Merge "Skip git-checkout related tests when .git is missing"\x00 +6c27ce7\x00Skip git-checkout related tests when .git is missing\x00 +451e513\x00Bug fix: create_stack() fails when waiting\x00 +4c8cfe4\x00Improve test coverage: network delete API\x00 (tag: refs/tags/(evil)) +d7e6167\x00Bug fix: Fix pass thru filtering in list_networks\x00 (tag: refs/tags/ev()il) +c47ec15\x00Consider 'in-use' a non-pending volume for caching\x00 (tag: refs/tags/ev)il) +8696fbd\x00Improve test coverage: private extension API\x00 (tag: refs/tags/ev(il) +f0440f8\x00Improve test coverage: hypervisor list\x00 (tag: refs/tags/e(vi)l) +04984a5\x00Refactor hooks file.\x00 (HEAD, tag: 0.6.7,b, tag: refs/tags/(12), refs/heads/master) +a65e8ee\x00Remove jinja pin.\x00 (tag: refs/tags/0.5.14, tag: refs/tags/0.5.13) +""" # noqa + + +def _make_old_git_changelog_format(line): + """Convert post-1.8.1 git log format to pre-1.8.1 git log format""" + + if not line.strip(): + return line + sha, msg, refname = line.split('\x00') + refname = refname.replace('tag: ', '') + return '\x00'.join((sha, msg, refname)) + +_old_git_changelog_content = '\n'.join( + _make_old_git_changelog_format(line) + for line in _changelog_content.split('\n')) + + +class GitLogsTest(base.BaseTestCase): + + scenarios = [ + ('pre1.8.3', {'changelog': _old_git_changelog_content}), + ('post1.8.3', {'changelog': _changelog_content}), + ] + + def setUp(self): + super(GitLogsTest, self).setUp() + self.temp_path = self.useFixture(fixtures.TempDir()).path + self.root_dir = os.path.abspath(os.path.curdir) + self.git_dir = os.path.join(self.root_dir, ".git") + self.useFixture( + fixtures.EnvironmentVariable('SKIP_GENERATE_AUTHORS')) + self.useFixture( + fixtures.EnvironmentVariable('SKIP_WRITE_GIT_CHANGELOG')) + + def test_write_git_changelog(self): + self.useFixture(fixtures.FakePopen(lambda _: { + "stdout": BytesIO(self.changelog.encode('utf-8')) + })) + + git.write_git_changelog(git_dir=self.git_dir, + dest_dir=self.temp_path) + + with open(os.path.join(self.temp_path, "ChangeLog"), "r") as ch_fh: + changelog_contents = ch_fh.read() + self.assertIn("2013.2", changelog_contents) + self.assertIn("0.5.17", changelog_contents) + self.assertIn("------", changelog_contents) + self.assertIn("Refactor hooks file", changelog_contents) + self.assertIn( + "Bug fix: create\_stack() fails when waiting", + changelog_contents) + self.assertNotIn("Refactor hooks file.", changelog_contents) + self.assertNotIn("182feb3", changelog_contents) + self.assertNotIn("review/monty_taylor/27519", changelog_contents) + self.assertNotIn("0.5.13", changelog_contents) + self.assertNotIn("0.6.7", changelog_contents) + self.assertNotIn("12", changelog_contents) + self.assertNotIn("(evil)", changelog_contents) + self.assertNotIn("ev()il", changelog_contents) + self.assertNotIn("ev(il", changelog_contents) + self.assertNotIn("ev)il", changelog_contents) + self.assertNotIn("e(vi)l", changelog_contents) + self.assertNotIn('Merge "', changelog_contents) + self.assertNotIn('1\_foo.1', changelog_contents) + + def test_generate_authors(self): + author_old = u"Foo Foo " + author_new = u"Bar Bar " + co_author = u"Foo Bar " + co_author_by = u"Co-authored-by: " + co_author + + git_log_cmd = ( + "git --git-dir=%s log --format=%%aN <%%aE>" + % self.git_dir) + git_co_log_cmd = ("git --git-dir=%s log" % self.git_dir) + git_top_level = "git rev-parse --show-toplevel" + cmd_map = { + git_log_cmd: author_new, + git_co_log_cmd: co_author_by, + git_top_level: self.root_dir, + } + + exist_files = [self.git_dir, + os.path.join(self.temp_path, "AUTHORS.in")] + self.useFixture(fixtures.MonkeyPatch( + "os.path.exists", + lambda path: os.path.abspath(path) in exist_files)) + + def _fake_run_shell_command(cmd, **kwargs): + return cmd_map[" ".join(cmd)] + + self.useFixture(fixtures.MonkeyPatch( + "pbr.git._run_shell_command", + _fake_run_shell_command)) + + with open(os.path.join(self.temp_path, "AUTHORS.in"), "w") as auth_fh: + auth_fh.write("%s\n" % author_old) + + git.generate_authors(git_dir=self.git_dir, + dest_dir=self.temp_path) + + with open(os.path.join(self.temp_path, "AUTHORS"), "r") as auth_fh: + authors = auth_fh.read() + self.assertTrue(author_old in authors) + self.assertTrue(author_new in authors) + self.assertTrue(co_author in authors) + + +class _SphinxConfig(object): + man_pages = ['foo'] + + +class BaseSphinxTest(base.BaseTestCase): + + def setUp(self): + super(BaseSphinxTest, self).setUp() + + # setup_command requires the Sphinx instance to have some + # attributes that aren't set normally with the way we use the + # class (because we replace the constructor). Add default + # values directly to the class definition. + import sphinx.application + sphinx.application.Sphinx.messagelog = [] + sphinx.application.Sphinx.statuscode = 0 + + self.useFixture(fixtures.MonkeyPatch( + "sphinx.application.Sphinx.__init__", lambda *a, **kw: None)) + self.useFixture(fixtures.MonkeyPatch( + "sphinx.application.Sphinx.build", lambda *a, **kw: None)) + self.useFixture(fixtures.MonkeyPatch( + "sphinx.application.Sphinx.config", _SphinxConfig)) + self.useFixture(fixtures.MonkeyPatch( + "sphinx.config.Config.init_values", lambda *a: None)) + self.useFixture(fixtures.MonkeyPatch( + "sphinx.config.Config.__init__", lambda *a: None)) + from distutils import dist + self.distr = dist.Distribution() + self.distr.packages = ("fake_package",) + self.distr.command_options["build_sphinx"] = { + "source_dir": ["a", "."]} + pkg_fixture = fixtures.PythonPackage( + "fake_package", [("fake_module.py", b""), + ("another_fake_module_for_testing.py", b""), + ("fake_private_module.py", b"")]) + self.useFixture(pkg_fixture) + self.useFixture(base.DiveDir(pkg_fixture.base)) + self.distr.command_options["pbr"] = {} + if hasattr(self, "excludes"): + self.distr.command_options["pbr"]["autodoc_exclude_modules"] = ( + 'setup.cfg', + "fake_package.fake_private_module\n" + "fake_package.another_fake_*\n" + "fake_package.unknown_module") + if hasattr(self, 'has_opt') and self.has_opt: + options = self.distr.command_options["pbr"] + options["autodoc_index_modules"] = ('setup.cfg', self.autodoc) + + +class BuildSphinxTest(BaseSphinxTest): + + scenarios = [ + ('true_autodoc_caps', + dict(has_opt=True, autodoc='True', has_autodoc=True)), + ('true_autodoc_caps_with_excludes', + dict(has_opt=True, autodoc='True', has_autodoc=True, + excludes="fake_package.fake_private_module\n" + "fake_package.another_fake_*\n" + "fake_package.unknown_module")), + ('true_autodoc_lower', + dict(has_opt=True, autodoc='true', has_autodoc=True)), + ('false_autodoc', + dict(has_opt=True, autodoc='False', has_autodoc=False)), + ('no_autodoc', + dict(has_opt=False, autodoc='False', has_autodoc=False)), + ] + + def test_build_doc(self): + build_doc = packaging.LocalBuildDoc(self.distr) + build_doc.run() + + self.assertTrue( + os.path.exists("api/autoindex.rst") == self.has_autodoc) + self.assertTrue( + os.path.exists( + "api/fake_package.fake_module.rst") == self.has_autodoc) + if not self.has_autodoc or hasattr(self, "excludes"): + assertion = self.assertFalse + else: + assertion = self.assertTrue + assertion( + os.path.exists( + "api/fake_package.fake_private_module.rst")) + assertion( + os.path.exists( + "api/fake_package.another_fake_module_for_testing.rst")) + + def test_builders_config(self): + build_doc = packaging.LocalBuildDoc(self.distr) + build_doc.finalize_options() + + self.assertEqual(1, len(build_doc.builders)) + self.assertIn('html', build_doc.builders) + + build_doc = packaging.LocalBuildDoc(self.distr) + build_doc.builders = '' + build_doc.finalize_options() + + self.assertEqual('', build_doc.builders) + + build_doc = packaging.LocalBuildDoc(self.distr) + build_doc.builders = 'man' + build_doc.finalize_options() + + self.assertEqual(1, len(build_doc.builders)) + self.assertIn('man', build_doc.builders) + + build_doc = packaging.LocalBuildDoc(self.distr) + build_doc.builders = 'html,man,doctest' + build_doc.finalize_options() + + self.assertIn('html', build_doc.builders) + self.assertIn('man', build_doc.builders) + self.assertIn('doctest', build_doc.builders) + + def test_cmd_builder_override(self): + + if self.has_opt: + self.distr.command_options["pbr"] = { + "autodoc_index_modules": ('setup.cfg', self.autodoc) + } + + self.distr.command_options["build_sphinx"]["builder"] = ( + "command line", "non-existing-builder") + + build_doc = packaging.LocalBuildDoc(self.distr) + self.assertNotIn('non-existing-builder', build_doc.builders) + self.assertIn('html', build_doc.builders) + + # process command line options which should override config + build_doc.finalize_options() + + self.assertIn('non-existing-builder', build_doc.builders) + self.assertNotIn('html', build_doc.builders) + + def test_cmd_builder_override_multiple_builders(self): + + if self.has_opt: + self.distr.command_options["pbr"] = { + "autodoc_index_modules": ('setup.cfg', self.autodoc) + } + + self.distr.command_options["build_sphinx"]["builder"] = ( + "command line", "builder1,builder2") + + build_doc = packaging.LocalBuildDoc(self.distr) + build_doc.finalize_options() + + self.assertEqual(["builder1", "builder2"], build_doc.builders) + + +class APIAutoDocTest(base.BaseTestCase): + + def setUp(self): + super(APIAutoDocTest, self).setUp() + + # setup_command requires the Sphinx instance to have some + # attributes that aren't set normally with the way we use the + # class (because we replace the constructor). Add default + # values directly to the class definition. + import sphinx.application + sphinx.application.Sphinx.messagelog = [] + sphinx.application.Sphinx.statuscode = 0 + + self.useFixture(fixtures.MonkeyPatch( + "sphinx.application.Sphinx.__init__", lambda *a, **kw: None)) + self.useFixture(fixtures.MonkeyPatch( + "sphinx.application.Sphinx.build", lambda *a, **kw: None)) + self.useFixture(fixtures.MonkeyPatch( + "sphinx.application.Sphinx.config", _SphinxConfig)) + self.useFixture(fixtures.MonkeyPatch( + "sphinx.config.Config.init_values", lambda *a: None)) + self.useFixture(fixtures.MonkeyPatch( + "sphinx.config.Config.__init__", lambda *a: None)) + from distutils import dist + self.distr = dist.Distribution() + self.distr.packages = ("fake_package",) + self.distr.command_options["build_sphinx"] = { + "source_dir": ["a", "."]} + self.sphinx_options = self.distr.command_options["build_sphinx"] + pkg_fixture = fixtures.PythonPackage( + "fake_package", [("fake_module.py", b""), + ("another_fake_module_for_testing.py", b""), + ("fake_private_module.py", b"")]) + self.useFixture(pkg_fixture) + self.useFixture(base.DiveDir(pkg_fixture.base)) + self.pbr_options = self.distr.command_options.setdefault('pbr', {}) + self.pbr_options["autodoc_index_modules"] = ('setup.cfg', 'True') + + def test_default_api_build_dir(self): + build_doc = packaging.LocalBuildDoc(self.distr) + build_doc.run() + + print('PBR OPTIONS:', self.pbr_options) + print('DISTR OPTIONS:', self.distr.command_options) + + self.assertTrue(os.path.exists("api/autoindex.rst")) + self.assertTrue(os.path.exists("api/fake_package.fake_module.rst")) + self.assertTrue( + os.path.exists( + "api/fake_package.fake_private_module.rst")) + self.assertTrue( + os.path.exists( + "api/fake_package.another_fake_module_for_testing.rst")) + + def test_different_api_build_dir(self): + # Options have to come out of the settings dict as a tuple + # showing the source and the value. + self.pbr_options['api_doc_dir'] = (None, 'contributor/api') + build_doc = packaging.LocalBuildDoc(self.distr) + build_doc.run() + + print('PBR OPTIONS:', self.pbr_options) + print('DISTR OPTIONS:', self.distr.command_options) + + self.assertTrue(os.path.exists("contributor/api/autoindex.rst")) + self.assertTrue( + os.path.exists("contributor/api/fake_package.fake_module.rst")) + self.assertTrue( + os.path.exists( + "contributor/api/fake_package.fake_private_module.rst")) diff --git a/libs/pbr/tests/test_util.py b/libs/pbr/tests/test_util.py new file mode 100644 index 00000000..370a7dee --- /dev/null +++ b/libs/pbr/tests/test_util.py @@ -0,0 +1,91 @@ +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. (HP) +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import io +import textwrap + +import six +from six.moves import configparser +import sys + +from pbr.tests import base +from pbr import util + + +class TestExtrasRequireParsingScenarios(base.BaseTestCase): + + scenarios = [ + ('simple_extras', { + 'config_text': """ + [extras] + first = + foo + bar==1.0 + second = + baz>=3.2 + foo + """, + 'expected_extra_requires': { + 'first': ['foo', 'bar==1.0'], + 'second': ['baz>=3.2', 'foo'], + 'test': ['requests-mock'], + "test:(python_version=='2.6')": ['ordereddict'], + } + }), + ('with_markers', { + 'config_text': """ + [extras] + test = + foo:python_version=='2.6' + bar + baz<1.6 :python_version=='2.6' + zaz :python_version>'1.0' + """, + 'expected_extra_requires': { + "test:(python_version=='2.6')": ['foo', 'baz<1.6'], + "test": ['bar', 'zaz']}}), + ('no_extras', { + 'config_text': """ + [metadata] + long_description = foo + """, + 'expected_extra_requires': + {} + })] + + def config_from_ini(self, ini): + config = {} + if sys.version_info >= (3, 2): + parser = configparser.ConfigParser() + else: + parser = configparser.SafeConfigParser() + ini = textwrap.dedent(six.u(ini)) + parser.readfp(io.StringIO(ini)) + for section in parser.sections(): + config[section] = dict(parser.items(section)) + return config + + def test_extras_parsing(self): + config = self.config_from_ini(self.config_text) + kwargs = util.setup_cfg_to_setup_kwargs(config) + + self.assertEqual(self.expected_extra_requires, + kwargs['extras_require']) + + +class TestInvalidMarkers(base.BaseTestCase): + + def test_invalid_marker_raises_error(self): + config = {'extras': {'test': "foo :bad_marker>'1.0'"}} + self.assertRaises(SyntaxError, util.setup_cfg_to_setup_kwargs, config) diff --git a/libs/pbr/tests/test_version.py b/libs/pbr/tests/test_version.py new file mode 100644 index 00000000..d861d572 --- /dev/null +++ b/libs/pbr/tests/test_version.py @@ -0,0 +1,311 @@ +# Copyright 2012 Red Hat, Inc. +# Copyright 2012-2013 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import itertools + +from testtools import matchers + +from pbr.tests import base +from pbr import version + + +from_pip_string = version.SemanticVersion.from_pip_string + + +class TestSemanticVersion(base.BaseTestCase): + + def test_ordering(self): + ordered_versions = [ + "1.2.3.dev6", + "1.2.3.dev7", + "1.2.3.a4.dev12", + "1.2.3.a4.dev13", + "1.2.3.a4", + "1.2.3.a5.dev1", + "1.2.3.a5", + "1.2.3.b3.dev1", + "1.2.3.b3", + "1.2.3.rc2.dev1", + "1.2.3.rc2", + "1.2.3.rc3.dev1", + "1.2.3", + "1.2.4", + "1.3.3", + "2.2.3", + ] + for v in ordered_versions: + sv = version.SemanticVersion.from_pip_string(v) + self.expectThat(sv, matchers.Equals(sv)) + for left, right in itertools.combinations(ordered_versions, 2): + l_pos = ordered_versions.index(left) + r_pos = ordered_versions.index(right) + if l_pos < r_pos: + m1 = matchers.LessThan + m2 = matchers.GreaterThan + else: + m1 = matchers.GreaterThan + m2 = matchers.LessThan + left_sv = version.SemanticVersion.from_pip_string(left) + right_sv = version.SemanticVersion.from_pip_string(right) + self.expectThat(left_sv, m1(right_sv)) + self.expectThat(right_sv, m2(left_sv)) + + def test_from_pip_string_legacy_alpha(self): + expected = version.SemanticVersion( + 1, 2, 0, prerelease_type='rc', prerelease=1) + parsed = from_pip_string('1.2.0rc1') + self.assertEqual(expected, parsed) + + def test_from_pip_string_legacy_postN(self): + # When pbr trunk was incompatible with PEP-440, a stable release was + # made that used postN versions to represent developer builds. As + # we expect only to be parsing versions of our own, we map those + # into dev builds of the next version. + expected = version.SemanticVersion(1, 2, 4, dev_count=5) + parsed = from_pip_string('1.2.3.post5') + self.expectThat(expected, matchers.Equals(parsed)) + expected = version.SemanticVersion(1, 2, 3, 'a', 5, dev_count=6) + parsed = from_pip_string('1.2.3.0a4.post6') + self.expectThat(expected, matchers.Equals(parsed)) + # We can't define a mapping for .postN.devM, so it should raise. + self.expectThat( + lambda: from_pip_string('1.2.3.post5.dev6'), + matchers.raises(ValueError)) + + def test_from_pip_string_v_version(self): + parsed = from_pip_string('v1.2.3') + expected = version.SemanticVersion(1, 2, 3) + self.expectThat(expected, matchers.Equals(parsed)) + + expected = version.SemanticVersion(1, 2, 3, 'a', 5, dev_count=6) + parsed = from_pip_string('V1.2.3.0a4.post6') + self.expectThat(expected, matchers.Equals(parsed)) + + self.expectThat( + lambda: from_pip_string('x1.2.3'), + matchers.raises(ValueError)) + + def test_from_pip_string_legacy_nonzero_lead_in(self): + # reported in bug 1361251 + expected = version.SemanticVersion( + 0, 0, 1, prerelease_type='a', prerelease=2) + parsed = from_pip_string('0.0.1a2') + self.assertEqual(expected, parsed) + + def test_from_pip_string_legacy_short_nonzero_lead_in(self): + expected = version.SemanticVersion( + 0, 1, 0, prerelease_type='a', prerelease=2) + parsed = from_pip_string('0.1a2') + self.assertEqual(expected, parsed) + + def test_from_pip_string_legacy_no_0_prerelease(self): + expected = version.SemanticVersion( + 2, 1, 0, prerelease_type='rc', prerelease=1) + parsed = from_pip_string('2.1.0.rc1') + self.assertEqual(expected, parsed) + + def test_from_pip_string_legacy_no_0_prerelease_2(self): + expected = version.SemanticVersion( + 2, 0, 0, prerelease_type='rc', prerelease=1) + parsed = from_pip_string('2.0.0.rc1') + self.assertEqual(expected, parsed) + + def test_from_pip_string_legacy_non_440_beta(self): + expected = version.SemanticVersion( + 2014, 2, prerelease_type='b', prerelease=2) + parsed = from_pip_string('2014.2.b2') + self.assertEqual(expected, parsed) + + def test_from_pip_string_pure_git_hash(self): + self.assertRaises(ValueError, from_pip_string, '6eed5ae') + + def test_from_pip_string_non_digit_start(self): + self.assertRaises(ValueError, from_pip_string, + 'non-release-tag/2014.12.16-1') + + def test_final_version(self): + semver = version.SemanticVersion(1, 2, 3) + self.assertEqual((1, 2, 3, 'final', 0), semver.version_tuple()) + self.assertEqual("1.2.3", semver.brief_string()) + self.assertEqual("1.2.3", semver.debian_string()) + self.assertEqual("1.2.3", semver.release_string()) + self.assertEqual("1.2.3", semver.rpm_string()) + self.assertEqual(semver, from_pip_string("1.2.3")) + + def test_parsing_short_forms(self): + semver = version.SemanticVersion(1, 0, 0) + self.assertEqual(semver, from_pip_string("1")) + self.assertEqual(semver, from_pip_string("1.0")) + self.assertEqual(semver, from_pip_string("1.0.0")) + + def test_dev_version(self): + semver = version.SemanticVersion(1, 2, 4, dev_count=5) + self.assertEqual((1, 2, 4, 'dev', 4), semver.version_tuple()) + self.assertEqual("1.2.4", semver.brief_string()) + self.assertEqual("1.2.4~dev5", semver.debian_string()) + self.assertEqual("1.2.4.dev5", semver.release_string()) + self.assertEqual("1.2.3.dev5", semver.rpm_string()) + self.assertEqual(semver, from_pip_string("1.2.4.dev5")) + + def test_dev_no_git_version(self): + semver = version.SemanticVersion(1, 2, 4, dev_count=5) + self.assertEqual((1, 2, 4, 'dev', 4), semver.version_tuple()) + self.assertEqual("1.2.4", semver.brief_string()) + self.assertEqual("1.2.4~dev5", semver.debian_string()) + self.assertEqual("1.2.4.dev5", semver.release_string()) + self.assertEqual("1.2.3.dev5", semver.rpm_string()) + self.assertEqual(semver, from_pip_string("1.2.4.dev5")) + + def test_dev_zero_version(self): + semver = version.SemanticVersion(1, 2, 0, dev_count=5) + self.assertEqual((1, 2, 0, 'dev', 4), semver.version_tuple()) + self.assertEqual("1.2.0", semver.brief_string()) + self.assertEqual("1.2.0~dev5", semver.debian_string()) + self.assertEqual("1.2.0.dev5", semver.release_string()) + self.assertEqual("1.1.9999.dev5", semver.rpm_string()) + self.assertEqual(semver, from_pip_string("1.2.0.dev5")) + + def test_alpha_dev_version(self): + semver = version.SemanticVersion(1, 2, 4, 'a', 1, 12) + self.assertEqual((1, 2, 4, 'alphadev', 12), semver.version_tuple()) + self.assertEqual("1.2.4", semver.brief_string()) + self.assertEqual("1.2.4~a1.dev12", semver.debian_string()) + self.assertEqual("1.2.4.0a1.dev12", semver.release_string()) + self.assertEqual("1.2.3.a1.dev12", semver.rpm_string()) + self.assertEqual(semver, from_pip_string("1.2.4.0a1.dev12")) + + def test_alpha_version(self): + semver = version.SemanticVersion(1, 2, 4, 'a', 1) + self.assertEqual((1, 2, 4, 'alpha', 1), semver.version_tuple()) + self.assertEqual("1.2.4", semver.brief_string()) + self.assertEqual("1.2.4~a1", semver.debian_string()) + self.assertEqual("1.2.4.0a1", semver.release_string()) + self.assertEqual("1.2.3.a1", semver.rpm_string()) + self.assertEqual(semver, from_pip_string("1.2.4.0a1")) + + def test_alpha_zero_version(self): + semver = version.SemanticVersion(1, 2, 0, 'a', 1) + self.assertEqual((1, 2, 0, 'alpha', 1), semver.version_tuple()) + self.assertEqual("1.2.0", semver.brief_string()) + self.assertEqual("1.2.0~a1", semver.debian_string()) + self.assertEqual("1.2.0.0a1", semver.release_string()) + self.assertEqual("1.1.9999.a1", semver.rpm_string()) + self.assertEqual(semver, from_pip_string("1.2.0.0a1")) + + def test_alpha_major_zero_version(self): + semver = version.SemanticVersion(1, 0, 0, 'a', 1) + self.assertEqual((1, 0, 0, 'alpha', 1), semver.version_tuple()) + self.assertEqual("1.0.0", semver.brief_string()) + self.assertEqual("1.0.0~a1", semver.debian_string()) + self.assertEqual("1.0.0.0a1", semver.release_string()) + self.assertEqual("0.9999.9999.a1", semver.rpm_string()) + self.assertEqual(semver, from_pip_string("1.0.0.0a1")) + + def test_alpha_default_version(self): + semver = version.SemanticVersion(1, 2, 4, 'a') + self.assertEqual((1, 2, 4, 'alpha', 0), semver.version_tuple()) + self.assertEqual("1.2.4", semver.brief_string()) + self.assertEqual("1.2.4~a0", semver.debian_string()) + self.assertEqual("1.2.4.0a0", semver.release_string()) + self.assertEqual("1.2.3.a0", semver.rpm_string()) + self.assertEqual(semver, from_pip_string("1.2.4.0a0")) + + def test_beta_dev_version(self): + semver = version.SemanticVersion(1, 2, 4, 'b', 1, 12) + self.assertEqual((1, 2, 4, 'betadev', 12), semver.version_tuple()) + self.assertEqual("1.2.4", semver.brief_string()) + self.assertEqual("1.2.4~b1.dev12", semver.debian_string()) + self.assertEqual("1.2.4.0b1.dev12", semver.release_string()) + self.assertEqual("1.2.3.b1.dev12", semver.rpm_string()) + self.assertEqual(semver, from_pip_string("1.2.4.0b1.dev12")) + + def test_beta_version(self): + semver = version.SemanticVersion(1, 2, 4, 'b', 1) + self.assertEqual((1, 2, 4, 'beta', 1), semver.version_tuple()) + self.assertEqual("1.2.4", semver.brief_string()) + self.assertEqual("1.2.4~b1", semver.debian_string()) + self.assertEqual("1.2.4.0b1", semver.release_string()) + self.assertEqual("1.2.3.b1", semver.rpm_string()) + self.assertEqual(semver, from_pip_string("1.2.4.0b1")) + + def test_decrement_nonrelease(self): + # The prior version of any non-release is a release + semver = version.SemanticVersion(1, 2, 4, 'b', 1) + self.assertEqual( + version.SemanticVersion(1, 2, 3), semver.decrement()) + + def test_decrement_nonrelease_zero(self): + # We set an arbitrary max version of 9999 when decrementing versions + # - this is part of handling rpm support. + semver = version.SemanticVersion(1, 0, 0) + self.assertEqual( + version.SemanticVersion(0, 9999, 9999), semver.decrement()) + + def test_decrement_release(self): + # The next patch version of a release version requires a change to the + # patch level. + semver = version.SemanticVersion(2, 2, 5) + self.assertEqual( + version.SemanticVersion(2, 2, 4), semver.decrement()) + + def test_increment_nonrelease(self): + # The next patch version of a non-release version is another + # non-release version as the next release doesn't need to be + # incremented. + semver = version.SemanticVersion(1, 2, 4, 'b', 1) + self.assertEqual( + version.SemanticVersion(1, 2, 4, 'b', 2), semver.increment()) + # Major and minor increments however need to bump things. + self.assertEqual( + version.SemanticVersion(1, 3, 0), semver.increment(minor=True)) + self.assertEqual( + version.SemanticVersion(2, 0, 0), semver.increment(major=True)) + + def test_increment_release(self): + # The next patch version of a release version requires a change to the + # patch level. + semver = version.SemanticVersion(1, 2, 5) + self.assertEqual( + version.SemanticVersion(1, 2, 6), semver.increment()) + self.assertEqual( + version.SemanticVersion(1, 3, 0), semver.increment(minor=True)) + self.assertEqual( + version.SemanticVersion(2, 0, 0), semver.increment(major=True)) + + def test_rc_dev_version(self): + semver = version.SemanticVersion(1, 2, 4, 'rc', 1, 12) + self.assertEqual((1, 2, 4, 'candidatedev', 12), semver.version_tuple()) + self.assertEqual("1.2.4", semver.brief_string()) + self.assertEqual("1.2.4~rc1.dev12", semver.debian_string()) + self.assertEqual("1.2.4.0rc1.dev12", semver.release_string()) + self.assertEqual("1.2.3.rc1.dev12", semver.rpm_string()) + self.assertEqual(semver, from_pip_string("1.2.4.0rc1.dev12")) + + def test_rc_version(self): + semver = version.SemanticVersion(1, 2, 4, 'rc', 1) + self.assertEqual((1, 2, 4, 'candidate', 1), semver.version_tuple()) + self.assertEqual("1.2.4", semver.brief_string()) + self.assertEqual("1.2.4~rc1", semver.debian_string()) + self.assertEqual("1.2.4.0rc1", semver.release_string()) + self.assertEqual("1.2.3.rc1", semver.rpm_string()) + self.assertEqual(semver, from_pip_string("1.2.4.0rc1")) + + def test_to_dev(self): + self.assertEqual( + version.SemanticVersion(1, 2, 3, dev_count=1), + version.SemanticVersion(1, 2, 3).to_dev(1)) + self.assertEqual( + version.SemanticVersion(1, 2, 3, 'rc', 1, dev_count=1), + version.SemanticVersion(1, 2, 3, 'rc', 1).to_dev(1)) diff --git a/libs/pbr/tests/test_wsgi.py b/libs/pbr/tests/test_wsgi.py new file mode 100644 index 00000000..f840610d --- /dev/null +++ b/libs/pbr/tests/test_wsgi.py @@ -0,0 +1,163 @@ +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. (HP) +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +import re +import subprocess +import sys +try: + # python 2 + from urllib2 import urlopen +except ImportError: + # python 3 + from urllib.request import urlopen + +from pbr.tests import base + + +class TestWsgiScripts(base.BaseTestCase): + + cmd_names = ('pbr_test_wsgi', 'pbr_test_wsgi_with_class') + + def _get_path(self): + if os.path.isdir("%s/lib64" % self.temp_dir): + path = "%s/lib64" % self.temp_dir + elif os.path.isdir("%s/lib" % self.temp_dir): + path = "%s/lib" % self.temp_dir + elif os.path.isdir("%s/site-packages" % self.temp_dir): + return ".:%s/site-packages" % self.temp_dir + else: + raise Exception("Could not determine path for test") + return ".:%s/python%s.%s/site-packages" % ( + path, + sys.version_info[0], + sys.version_info[1]) + + def test_wsgi_script_install(self): + """Test that we install a non-pkg-resources wsgi script.""" + if os.name == 'nt': + self.skipTest('Windows support is passthrough') + + stdout, _, return_code = self.run_setup( + 'install', '--prefix=%s' % self.temp_dir) + + self._check_wsgi_install_content(stdout) + + def test_wsgi_script_run(self): + """Test that we install a runnable wsgi script. + + This test actually attempts to start and interact with the + wsgi script in question to demonstrate that it's a working + wsgi script using simple server. + + """ + if os.name == 'nt': + self.skipTest('Windows support is passthrough') + + stdout, _, return_code = self.run_setup( + 'install', '--prefix=%s' % self.temp_dir) + + self._check_wsgi_install_content(stdout) + + # Live test run the scripts and see that they respond to wsgi + # requests. + for cmd_name in self.cmd_names: + self._test_wsgi(cmd_name, b'Hello World') + + def _test_wsgi(self, cmd_name, output, extra_args=None): + cmd = os.path.join(self.temp_dir, 'bin', cmd_name) + print("Running %s -p 0" % cmd) + popen_cmd = [cmd, '-p', '0'] + if extra_args: + popen_cmd.extend(extra_args) + + env = {'PYTHONPATH': self._get_path()} + + p = subprocess.Popen(popen_cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, cwd=self.temp_dir, + env=env) + self.addCleanup(p.kill) + + stdoutdata = p.stdout.readline() # ****... + + stdoutdata = p.stdout.readline() # STARTING test server... + self.assertIn( + b"STARTING test server pbr_testpackage.wsgi", + stdoutdata) + + stdoutdata = p.stdout.readline() # Available at ... + print(stdoutdata) + m = re.search(b'(http://[^:]+:\d+)/', stdoutdata) + self.assertIsNotNone(m, "Regex failed to match on %s" % stdoutdata) + + stdoutdata = p.stdout.readline() # DANGER! ... + self.assertIn( + b"DANGER! For testing only, do not use in production", + stdoutdata) + + stdoutdata = p.stdout.readline() # ***... + + f = urlopen(m.group(1).decode('utf-8')) + self.assertEqual(output, f.read()) + + # Request again so that the application can force stderr.flush(), + # otherwise the log is buffered and the next readline() will hang. + urlopen(m.group(1).decode('utf-8')) + + stdoutdata = p.stderr.readline() + # we should have logged an HTTP request, return code 200, that + # returned the right amount of bytes + status = '"GET / HTTP/1.1" 200 %d' % len(output) + self.assertIn(status.encode('utf-8'), stdoutdata) + + def _check_wsgi_install_content(self, install_stdout): + for cmd_name in self.cmd_names: + install_txt = 'Installing %s script to %s' % (cmd_name, + self.temp_dir) + self.assertIn(install_txt, install_stdout) + + cmd_filename = os.path.join(self.temp_dir, 'bin', cmd_name) + + script_txt = open(cmd_filename, 'r').read() + self.assertNotIn('pkg_resources', script_txt) + + main_block = """if __name__ == "__main__": + import argparse + import socket + import sys + import wsgiref.simple_server as wss""" + + if cmd_name == 'pbr_test_wsgi': + app_name = "main" + else: + app_name = "WSGI.app" + + starting_block = ("STARTING test server pbr_testpackage.wsgi." + "%s" % app_name) + + else_block = """else: + application = None""" + + self.assertIn(main_block, script_txt) + self.assertIn(starting_block, script_txt) + self.assertIn(else_block, script_txt) + + def test_with_argument(self): + if os.name == 'nt': + self.skipTest('Windows support is passthrough') + + stdout, _, return_code = self.run_setup( + 'install', '--prefix=%s' % self.temp_dir) + + self._test_wsgi('pbr_test_wsgi', b'Foo Bar', ["--", "-c", "Foo Bar"]) diff --git a/libs/pbr/tests/testpackage/CHANGES.txt b/libs/pbr/tests/testpackage/CHANGES.txt new file mode 100644 index 00000000..709b9d4c --- /dev/null +++ b/libs/pbr/tests/testpackage/CHANGES.txt @@ -0,0 +1,86 @@ +Changelog +=========== + +0.3 (unreleased) +------------------ + +- The ``glob_data_files`` hook became a pre-command hook for the install_data + command instead of being a setup-hook. This is to support the additional + functionality of requiring data_files with relative destination paths to be + install relative to the package's install path (i.e. site-packages). + +- Dropped support for and deprecated the easier_install custom command. + Although it should still work, it probably won't be used anymore for + stsci_python packages. + +- Added support for the ``build_optional_ext`` command, which replaces/extends + the default ``build_ext`` command. See the README for more details. + +- Added the ``tag_svn_revision`` setup_hook as a replacement for the + setuptools-specific tag_svn_revision option to the egg_info command. This + new hook is easier to use than the old tag_svn_revision option: It's + automatically enabled by the presence of ``.dev`` in the version string, and + disabled otherwise. + +- The ``svn_info_pre_hook`` and ``svn_info_post_hook`` have been replaced with + ``version_pre_command_hook`` and ``version_post_command_hook`` respectively. + However, a new ``version_setup_hook``, which has the same purpose, has been + added. It is generally easier to use and will give more consistent results + in that it will run every time setup.py is run, regardless of which command + is used. ``stsci.distutils`` itself uses this hook--see the `setup.cfg` file + and `stsci/distutils/__init__.py` for example usage. + +- Instead of creating an `svninfo.py` module, the new ``version_`` hooks create + a file called `version.py`. In addition to the SVN info that was included + in `svninfo.py`, it includes a ``__version__`` variable to be used by the + package's `__init__.py`. This allows there to be a hard-coded + ``__version__`` variable included in the source code, rather than using + pkg_resources to get the version. + +- In `version.py`, the variables previously named ``__svn_version__`` and + ``__full_svn_info__`` are now named ``__svn_revision__`` and + ``__svn_full_info__``. + +- Fixed a bug when using stsci.distutils in the installation of other packages + in the ``stsci.*`` namespace package. If stsci.distutils was not already + installed, and was downloaded automatically by distribute through the + setup_requires option, then ``stsci.distutils`` would fail to import. This + is because the way the namespace package (nspkg) mechanism currently works, + all packages belonging to the nspkg *must* be on the import path at initial + import time. + + So when installing stsci.tools, for example, if ``stsci.tools`` is imported + from within the source code at install time, but before ``stsci.distutils`` + is downloaded and added to the path, the ``stsci`` package is already + imported and can't be extended to include the path of ``stsci.distutils`` + after the fact. The easiest way of dealing with this, it seems, is to + delete ``stsci`` from ``sys.modules``, which forces it to be reimported, now + the its ``__path__`` extended to include ``stsci.distutil``'s path. + + +0.2.2 (2011-11-09) +------------------ + +- Fixed check for the issue205 bug on actual setuptools installs; before it + only worked on distribute. setuptools has the issue205 bug prior to version + 0.6c10. + +- Improved the fix for the issue205 bug, especially on setuptools. + setuptools, prior to 0.6c10, did not back of sys.modules either before + sandboxing, which causes serious problems. In fact, it's so bad that it's + not enough to add a sys.modules backup to the current sandbox: It's in fact + necessary to monkeypatch setuptools.sandbox.run_setup so that any subsequent + calls to it also back up sys.modules. + + +0.2.1 (2011-09-02) +------------------ + +- Fixed the dependencies so that setuptools is requirement but 'distribute' + specifically. Previously installation could fail if users had plain + setuptools installed and not distribute + +0.2 (2011-08-23) +------------------ + +- Initial public release diff --git a/libs/pbr/tests/testpackage/LICENSE.txt b/libs/pbr/tests/testpackage/LICENSE.txt new file mode 100644 index 00000000..7e8019a8 --- /dev/null +++ b/libs/pbr/tests/testpackage/LICENSE.txt @@ -0,0 +1,29 @@ +Copyright (C) 2005 Association of Universities for Research in Astronomy (AURA) + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + 3. The name of AURA and its representatives may not be used to + endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + diff --git a/libs/pbr/tests/testpackage/MANIFEST.in b/libs/pbr/tests/testpackage/MANIFEST.in new file mode 100644 index 00000000..2e35f3ed --- /dev/null +++ b/libs/pbr/tests/testpackage/MANIFEST.in @@ -0,0 +1,2 @@ +include data_files/* +exclude pbr_testpackage/extra.py diff --git a/libs/pbr/tests/testpackage/README.txt b/libs/pbr/tests/testpackage/README.txt new file mode 100644 index 00000000..b6d84a7b --- /dev/null +++ b/libs/pbr/tests/testpackage/README.txt @@ -0,0 +1,148 @@ +Introduction +============ +This package contains utilities used to package some of STScI's Python +projects; specifically those projects that comprise stsci_python_ and +Astrolib_. + +It currently consists mostly of some setup_hook scripts meant for use with +`distutils2/packaging`_ and/or pbr_, and a customized easy_install command +meant for use with distribute_. + +This package is not meant for general consumption, though it might be worth +looking at for examples of how to do certain things with your own packages, but +YMMV. + +Features +======== + +Hook Scripts +------------ +Currently the main features of this package are a couple of setup_hook scripts. +In distutils2, a setup_hook is a script that runs at the beginning of any +pysetup command, and can modify the package configuration read from setup.cfg. +There are also pre- and post-command hooks that only run before/after a +specific setup command (eg. build_ext, install) is run. + +stsci.distutils.hooks.use_packages_root +''''''''''''''''''''''''''''''''''''''' +If using the ``packages_root`` option under the ``[files]`` section of +setup.cfg, this hook will add that path to ``sys.path`` so that modules in your +package can be imported and used in setup. This can be used even if +``packages_root`` is not specified--in this case it adds ``''`` to +``sys.path``. + +stsci.distutils.hooks.version_setup_hook +'''''''''''''''''''''''''''''''''''''''' +Creates a Python module called version.py which currently contains four +variables: + +* ``__version__`` (the release version) +* ``__svn_revision__`` (the SVN revision info as returned by the ``svnversion`` + command) +* ``__svn_full_info__`` (as returned by the ``svn info`` command) +* ``__setup_datetime__`` (the date and time that setup.py was last run). + +These variables can be imported in the package's `__init__.py` for degugging +purposes. The version.py module will *only* be created in a package that +imports from the version module in its `__init__.py`. It should be noted that +this is generally preferable to writing these variables directly into +`__init__.py`, since this provides more control and is less likely to +unexpectedly break things in `__init__.py`. + +stsci.distutils.hooks.version_pre_command_hook +'''''''''''''''''''''''''''''''''''''''''''''' +Identical to version_setup_hook, but designed to be used as a pre-command +hook. + +stsci.distutils.hooks.version_post_command_hook +''''''''''''''''''''''''''''''''''''''''''''''' +The complement to version_pre_command_hook. This will delete any version.py +files created during a build in order to prevent them from cluttering an SVN +working copy (note, however, that version.py is *not* deleted from the build/ +directory, so a copy of it is still preserved). It will also not be deleted +if the current directory is not an SVN working copy. For example, if source +code extracted from a source tarball it will be preserved. + +stsci.distutils.hooks.tag_svn_revision +'''''''''''''''''''''''''''''''''''''' +A setup_hook to add the SVN revision of the current working copy path to the +package version string, but only if the version ends in .dev. + +For example, ``mypackage-1.0.dev`` becomes ``mypackage-1.0.dev1234``. This is +in accordance with the version string format standardized by PEP 386. + +This should be used as a replacement for the ``tag_svn_revision`` option to +the egg_info command. This hook is more compatible with packaging/distutils2, +which does not include any VCS support. This hook is also more flexible in +that it turns the revision number on/off depending on the presence of ``.dev`` +in the version string, so that it's not automatically added to the version in +final releases. + +This hook does require the ``svnversion`` command to be available in order to +work. It does not examine the working copy metadata directly. + +stsci.distutils.hooks.numpy_extension_hook +'''''''''''''''''''''''''''''''''''''''''' +This is a pre-command hook for the build_ext command. To use it, add a +``[build_ext]`` section to your setup.cfg, and add to it:: + + pre-hook.numpy-extension-hook = stsci.distutils.hooks.numpy_extension_hook + +This hook must be used to build extension modules that use Numpy. The primary +side-effect of this hook is to add the correct numpy include directories to +`include_dirs`. To use it, add 'numpy' to the 'include-dirs' option of each +extension module that requires numpy to build. The value 'numpy' will be +replaced with the actual path to the numpy includes. + +stsci.distutils.hooks.is_display_option +''''''''''''''''''''''''''''''''''''''' +This is not actually a hook, but is a useful utility function that can be used +in writing other hooks. Basically, it returns ``True`` if setup.py was run +with a "display option" such as --version or --help. This can be used to +prevent your hook from running in such cases. + +stsci.distutils.hooks.glob_data_files +''''''''''''''''''''''''''''''''''''' +A pre-command hook for the install_data command. Allows filename wildcards as +understood by ``glob.glob()`` to be used in the data_files option. This hook +must be used in order to have this functionality since it does not normally +exist in distutils. + +This hook also ensures that data files are installed relative to the package +path. data_files shouldn't normally be installed this way, but the +functionality is required for a few special cases. + + +Commands +-------- +build_optional_ext +'''''''''''''''''' +This serves as an optional replacement for the default built_ext command, +which compiles C extension modules. Its purpose is to allow extension modules +to be *optional*, so that if their build fails the rest of the package is +still allowed to be built and installed. This can be used when an extension +module is not definitely required to use the package. + +To use this custom command, add:: + + commands = stsci.distutils.command.build_optional_ext.build_optional_ext + +under the ``[global]`` section of your package's setup.cfg. Then, to mark +an individual extension module as optional, under the setup.cfg section for +that extension add:: + + optional = True + +Optionally, you may also add a custom failure message by adding:: + + fail_message = The foobar extension module failed to compile. + This could be because you lack such and such headers. + This package will still work, but such and such features + will be disabled. + + +.. _stsci_python: http://www.stsci.edu/resources/software_hardware/pyraf/stsci_python +.. _Astrolib: http://www.scipy.org/AstroLib/ +.. _distutils2/packaging: http://distutils2.notmyidea.org/ +.. _d2to1: http://pypi.python.org/pypi/d2to1 +.. _distribute: http://pypi.python.org/pypi/distribute diff --git a/libs/pbr/tests/testpackage/data_files/a.txt b/libs/pbr/tests/testpackage/data_files/a.txt new file mode 100644 index 00000000..e69de29b diff --git a/libs/pbr/tests/testpackage/data_files/b.txt b/libs/pbr/tests/testpackage/data_files/b.txt new file mode 100644 index 00000000..e69de29b diff --git a/libs/pbr/tests/testpackage/data_files/c.rst b/libs/pbr/tests/testpackage/data_files/c.rst new file mode 100644 index 00000000..e69de29b diff --git a/libs/pbr/tests/testpackage/doc/source/conf.py b/libs/pbr/tests/testpackage/doc/source/conf.py new file mode 100644 index 00000000..73585100 --- /dev/null +++ b/libs/pbr/tests/testpackage/doc/source/conf.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys + +sys.path.insert(0, os.path.abspath('../..')) +# -- General configuration ---------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [ + 'sphinx.ext.autodoc', + #'sphinx.ext.intersphinx', +] + +# autodoc generation is a bit aggressive and a nuisance when doing heavy +# text edit cycles. +# execute "export SPHINX_DEBUG=1" in your terminal to disable + +# The suffix of source filenames. +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'testpackage' +copyright = u'2013, OpenStack Foundation' + +# If true, '()' will be appended to :func: etc. cross-reference text. +add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +add_module_names = True + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# -- Options for HTML output -------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +# html_theme_path = ["."] +# html_theme = '_theme' +# html_static_path = ['static'] + +# Output file base name for HTML help builder. +htmlhelp_basename = '%sdoc' % project + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass +# [howto/manual]). +latex_documents = [ + ('index', + '%s.tex' % project, + u'%s Documentation' % project, + u'OpenStack Foundation', 'manual'), +] + +# Example configuration for intersphinx: refer to the Python standard library. +#intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/libs/pbr/tests/testpackage/doc/source/index.rst b/libs/pbr/tests/testpackage/doc/source/index.rst new file mode 100644 index 00000000..9ce317fd --- /dev/null +++ b/libs/pbr/tests/testpackage/doc/source/index.rst @@ -0,0 +1,23 @@ +.. testpackage documentation master file, created by + sphinx-quickstart on Tue Jul 9 22:26:36 2013. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to testpackage's documentation! +======================================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + installation + usage + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/libs/pbr/tests/testpackage/doc/source/installation.rst b/libs/pbr/tests/testpackage/doc/source/installation.rst new file mode 100644 index 00000000..65bca43f --- /dev/null +++ b/libs/pbr/tests/testpackage/doc/source/installation.rst @@ -0,0 +1,12 @@ +============ +Installation +============ + +At the command line:: + + $ pip install testpackage + +Or, if you have virtualenvwrapper installed:: + + $ mkvirtualenv testpackage + $ pip install testpackage diff --git a/libs/pbr/tests/testpackage/doc/source/usage.rst b/libs/pbr/tests/testpackage/doc/source/usage.rst new file mode 100644 index 00000000..af97d795 --- /dev/null +++ b/libs/pbr/tests/testpackage/doc/source/usage.rst @@ -0,0 +1,7 @@ +======== +Usage +======== + +To use testpackage in a project:: + + import testpackage diff --git a/libs/pbr/tests/testpackage/extra-file.txt b/libs/pbr/tests/testpackage/extra-file.txt new file mode 100644 index 00000000..e69de29b diff --git a/libs/pbr/tests/testpackage/git-extra-file.txt b/libs/pbr/tests/testpackage/git-extra-file.txt new file mode 100644 index 00000000..e69de29b diff --git a/libs/pbr/tests/testpackage/pbr_testpackage/__init__.py b/libs/pbr/tests/testpackage/pbr_testpackage/__init__.py new file mode 100644 index 00000000..aa56dc6f --- /dev/null +++ b/libs/pbr/tests/testpackage/pbr_testpackage/__init__.py @@ -0,0 +1,3 @@ +import pbr.version + +__version__ = pbr.version.VersionInfo('pbr_testpackage').version_string() diff --git a/libs/pbr/tests/testpackage/pbr_testpackage/_setup_hooks.py b/libs/pbr/tests/testpackage/pbr_testpackage/_setup_hooks.py new file mode 100644 index 00000000..f8b30876 --- /dev/null +++ b/libs/pbr/tests/testpackage/pbr_testpackage/_setup_hooks.py @@ -0,0 +1,65 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Copyright (C) 2013 Association of Universities for Research in Astronomy +# (AURA) +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# +# 3. The name of AURA and its representatives may not be used to +# endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR IMPLIED +# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + +from distutils.command import build_py + + +def test_hook_1(config): + print('test_hook_1') + + +def test_hook_2(config): + print('test_hook_2') + + +class test_command(build_py.build_py): + command_name = 'build_py' + + def run(self): + print('Running custom build_py command.') + return build_py.build_py.run(self) + + +def test_pre_hook(cmdobj): + print('build_ext pre-hook') + + +def test_post_hook(cmdobj): + print('build_ext post-hook') diff --git a/libs/pbr/tests/testpackage/pbr_testpackage/cmd.py b/libs/pbr/tests/testpackage/pbr_testpackage/cmd.py new file mode 100644 index 00000000..4cc4522f --- /dev/null +++ b/libs/pbr/tests/testpackage/pbr_testpackage/cmd.py @@ -0,0 +1,26 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import print_function + + +def main(): + print("PBR Test Command") + + +class Foo(object): + + @classmethod + def bar(self): + print("PBR Test Command - with class!") diff --git a/libs/pbr/tests/testpackage/pbr_testpackage/extra.py b/libs/pbr/tests/testpackage/pbr_testpackage/extra.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/pbr/tests/testpackage/pbr_testpackage/package_data/1.txt b/libs/pbr/tests/testpackage/pbr_testpackage/package_data/1.txt new file mode 100644 index 00000000..e69de29b diff --git a/libs/pbr/tests/testpackage/pbr_testpackage/package_data/2.txt b/libs/pbr/tests/testpackage/pbr_testpackage/package_data/2.txt new file mode 100644 index 00000000..e69de29b diff --git a/libs/pbr/tests/testpackage/pbr_testpackage/wsgi.py b/libs/pbr/tests/testpackage/pbr_testpackage/wsgi.py new file mode 100644 index 00000000..1edd54d3 --- /dev/null +++ b/libs/pbr/tests/testpackage/pbr_testpackage/wsgi.py @@ -0,0 +1,40 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import print_function + +import argparse +import functools +import sys + + +def application(env, start_response, data): + sys.stderr.flush() # Force the previous request log to be written. + start_response('200 OK', [('Content-Type', 'text/html')]) + return [data.encode('utf-8')] + + +def main(): + parser = argparse.ArgumentParser(description='Return a string.') + parser.add_argument('--content', '-c', help='String returned', + default='Hello World') + args = parser.parse_args() + return functools.partial(application, data=args.content) + + +class WSGI(object): + + @classmethod + def app(self): + return functools.partial(application, data='Hello World') diff --git a/libs/pbr/tests/testpackage/setup.py b/libs/pbr/tests/testpackage/setup.py new file mode 100644 index 00000000..2d9f685b --- /dev/null +++ b/libs/pbr/tests/testpackage/setup.py @@ -0,0 +1,21 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import setuptools + +setuptools.setup( + setup_requires=['pbr'], + pbr=True, +) diff --git a/libs/pbr/tests/testpackage/src/testext.c b/libs/pbr/tests/testpackage/src/testext.c new file mode 100644 index 00000000..1b366e9b --- /dev/null +++ b/libs/pbr/tests/testpackage/src/testext.c @@ -0,0 +1,29 @@ +#include + + +static PyMethodDef TestextMethods[] = { + {NULL, NULL, 0, NULL} +}; + + +#if PY_MAJOR_VERSION >=3 +static struct PyModuleDef testextmodule = { + PyModuleDef_HEAD_INIT, /* This should correspond to a PyModuleDef_Base type */ + "testext", /* This is the module name */ + "Test extension module", /* This is the module docstring */ + -1, /* This defines the size of the module and says everything is global */ + TestextMethods /* This is the method definition */ +}; + +PyObject* +PyInit_testext(void) +{ + return PyModule_Create(&testextmodule); +} +#else +PyMODINIT_FUNC +inittestext(void) +{ + Py_InitModule("testext", TestextMethods); +} +#endif diff --git a/libs/pbr/tests/testpackage/test-requirements.txt b/libs/pbr/tests/testpackage/test-requirements.txt new file mode 100644 index 00000000..8755eb4c --- /dev/null +++ b/libs/pbr/tests/testpackage/test-requirements.txt @@ -0,0 +1,2 @@ +ordereddict;python_version=='2.6' +requests-mock diff --git a/libs/pbr/tests/util.py b/libs/pbr/tests/util.py new file mode 100644 index 00000000..0e7bcf15 --- /dev/null +++ b/libs/pbr/tests/util.py @@ -0,0 +1,78 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Copyright (C) 2013 Association of Universities for Research in Astronomy +# (AURA) +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# +# 3. The name of AURA and its representatives may not be used to +# endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR IMPLIED +# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + +import contextlib +import os +import shutil +import stat +import sys + +try: + import ConfigParser as configparser +except ImportError: + import configparser + + +@contextlib.contextmanager +def open_config(filename): + if sys.version_info >= (3, 2): + cfg = configparser.ConfigParser() + else: + cfg = configparser.SafeConfigParser() + cfg.read(filename) + yield cfg + with open(filename, 'w') as fp: + cfg.write(fp) + + +def rmtree(path): + """shutil.rmtree() with error handler. + + Handle 'access denied' from trying to delete read-only files. + """ + + def onerror(func, path, exc_info): + if not os.access(path, os.W_OK): + os.chmod(path, stat.S_IWUSR) + func(path) + else: + raise + + return shutil.rmtree(path, onerror=onerror) diff --git a/libs/pbr/util.py b/libs/pbr/util.py new file mode 100644 index 00000000..63e913d9 --- /dev/null +++ b/libs/pbr/util.py @@ -0,0 +1,611 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Copyright (C) 2013 Association of Universities for Research in Astronomy +# (AURA) +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# +# 3. The name of AURA and its representatives may not be used to +# endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR IMPLIED +# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +# DAMAGE. + +"""The code in this module is mostly copy/pasted out of the distutils2 source +code, as recommended by Tarek Ziade. As such, it may be subject to some change +as distutils2 development continues, and will have to be kept up to date. + +I didn't want to use it directly from distutils2 itself, since I do not want it +to be an installation dependency for our packages yet--it is still too unstable +(the latest version on PyPI doesn't even install). +""" + +# These first two imports are not used, but are needed to get around an +# irritating Python bug that can crop up when using ./setup.py test. +# See: http://www.eby-sarna.com/pipermail/peak/2010-May/003355.html +try: + import multiprocessing # noqa +except ImportError: + pass +import logging # noqa + +from collections import defaultdict +import os +import re +import sys +import traceback + +import distutils.ccompiler +from distutils import errors +from distutils import log +import pkg_resources +from setuptools import dist as st_dist +from setuptools import extension + +try: + import ConfigParser as configparser +except ImportError: + import configparser + +from pbr import extra_files +import pbr.hooks + +# A simplified RE for this; just checks that the line ends with version +# predicates in () +_VERSION_SPEC_RE = re.compile(r'\s*(.*?)\s*\((.*)\)\s*$') + + +# Mappings from setup() keyword arguments to setup.cfg options; +# The values are (section, option) tuples, or simply (section,) tuples if +# the option has the same name as the setup() argument +D1_D2_SETUP_ARGS = { + "name": ("metadata",), + "version": ("metadata",), + "author": ("metadata",), + "author_email": ("metadata",), + "maintainer": ("metadata",), + "maintainer_email": ("metadata",), + "url": ("metadata", "home_page"), + "project_urls": ("metadata",), + "description": ("metadata", "summary"), + "keywords": ("metadata",), + "long_description": ("metadata", "description"), + "long_description_content_type": ("metadata", "description_content_type"), + "download_url": ("metadata",), + "classifiers": ("metadata", "classifier"), + "platforms": ("metadata", "platform"), # ** + "license": ("metadata",), + # Use setuptools install_requires, not + # broken distutils requires + "install_requires": ("metadata", "requires_dist"), + "setup_requires": ("metadata", "setup_requires_dist"), + "python_requires": ("metadata",), + "provides": ("metadata", "provides_dist"), # ** + "obsoletes": ("metadata", "obsoletes_dist"), # ** + "package_dir": ("files", 'packages_root'), + "packages": ("files",), + "package_data": ("files",), + "namespace_packages": ("files",), + "data_files": ("files",), + "scripts": ("files",), + "py_modules": ("files", "modules"), # ** + "cmdclass": ("global", "commands"), + # Not supported in distutils2, but provided for + # backwards compatibility with setuptools + "use_2to3": ("backwards_compat", "use_2to3"), + "zip_safe": ("backwards_compat", "zip_safe"), + "tests_require": ("backwards_compat", "tests_require"), + "dependency_links": ("backwards_compat",), + "include_package_data": ("backwards_compat",), +} + +# setup() arguments that can have multiple values in setup.cfg +MULTI_FIELDS = ("classifiers", + "platforms", + "install_requires", + "provides", + "obsoletes", + "namespace_packages", + "packages", + "package_data", + "data_files", + "scripts", + "py_modules", + "dependency_links", + "setup_requires", + "tests_require", + "cmdclass") + +# setup() arguments that can have mapping values in setup.cfg +MAP_FIELDS = ("project_urls",) + +# setup() arguments that contain boolean values +BOOL_FIELDS = ("use_2to3", "zip_safe", "include_package_data") + + +CSV_FIELDS = ("keywords",) + + +def resolve_name(name): + """Resolve a name like ``module.object`` to an object and return it. + + Raise ImportError if the module or name is not found. + """ + + parts = name.split('.') + cursor = len(parts) - 1 + module_name = parts[:cursor] + attr_name = parts[-1] + + while cursor > 0: + try: + ret = __import__('.'.join(module_name), fromlist=[attr_name]) + break + except ImportError: + if cursor == 0: + raise + cursor -= 1 + module_name = parts[:cursor] + attr_name = parts[cursor] + ret = '' + + for part in parts[cursor:]: + try: + ret = getattr(ret, part) + except AttributeError: + raise ImportError(name) + + return ret + + +def cfg_to_args(path='setup.cfg', script_args=()): + """Distutils2 to distutils1 compatibility util. + + This method uses an existing setup.cfg to generate a dictionary of + keywords that can be used by distutils.core.setup(kwargs**). + + :param path: + The setup.cfg path. + :param script_args: + List of commands setup.py was called with. + :raises DistutilsFileError: + When the setup.cfg file is not found. + """ + + # The method source code really starts here. + if sys.version_info >= (3, 2): + parser = configparser.ConfigParser() + else: + parser = configparser.SafeConfigParser() + if not os.path.exists(path): + raise errors.DistutilsFileError("file '%s' does not exist" % + os.path.abspath(path)) + try: + parser.read(path, encoding='utf-8') + except TypeError: + # Python 2 doesn't accept the encoding kwarg + parser.read(path) + config = {} + for section in parser.sections(): + config[section] = dict() + for k, value in parser.items(section): + config[section][k.replace('-', '_')] = value + + # Run setup_hooks, if configured + setup_hooks = has_get_option(config, 'global', 'setup_hooks') + package_dir = has_get_option(config, 'files', 'packages_root') + + # Add the source package directory to sys.path in case it contains + # additional hooks, and to make sure it's on the path before any existing + # installations of the package + if package_dir: + package_dir = os.path.abspath(package_dir) + sys.path.insert(0, package_dir) + + try: + if setup_hooks: + setup_hooks = [ + hook for hook in split_multiline(setup_hooks) + if hook != 'pbr.hooks.setup_hook'] + for hook in setup_hooks: + hook_fn = resolve_name(hook) + try: + hook_fn(config) + except SystemExit: + log.error('setup hook %s terminated the installation') + except Exception: + e = sys.exc_info()[1] + log.error('setup hook %s raised exception: %s\n' % + (hook, e)) + log.error(traceback.format_exc()) + sys.exit(1) + + # Run the pbr hook + pbr.hooks.setup_hook(config) + + kwargs = setup_cfg_to_setup_kwargs(config, script_args) + + # Set default config overrides + kwargs['include_package_data'] = True + kwargs['zip_safe'] = False + + register_custom_compilers(config) + + ext_modules = get_extension_modules(config) + if ext_modules: + kwargs['ext_modules'] = ext_modules + + entry_points = get_entry_points(config) + if entry_points: + kwargs['entry_points'] = entry_points + + # Handle the [files]/extra_files option + files_extra_files = has_get_option(config, 'files', 'extra_files') + if files_extra_files: + extra_files.set_extra_files(split_multiline(files_extra_files)) + + finally: + # Perform cleanup if any paths were added to sys.path + if package_dir: + sys.path.pop(0) + + return kwargs + + +def setup_cfg_to_setup_kwargs(config, script_args=()): + """Convert config options to kwargs. + + Processes the setup.cfg options and converts them to arguments accepted + by setuptools' setup() function. + """ + + kwargs = {} + + # Temporarily holds install_requires and extra_requires while we + # parse env_markers. + all_requirements = {} + + for arg in D1_D2_SETUP_ARGS: + if len(D1_D2_SETUP_ARGS[arg]) == 2: + # The distutils field name is different than distutils2's. + section, option = D1_D2_SETUP_ARGS[arg] + + elif len(D1_D2_SETUP_ARGS[arg]) == 1: + # The distutils field name is the same thant distutils2's. + section = D1_D2_SETUP_ARGS[arg][0] + option = arg + + in_cfg_value = has_get_option(config, section, option) + if not in_cfg_value: + # There is no such option in the setup.cfg + if arg == "long_description": + in_cfg_value = has_get_option(config, section, + "description_file") + if in_cfg_value: + in_cfg_value = split_multiline(in_cfg_value) + value = '' + for filename in in_cfg_value: + description_file = open(filename) + try: + value += description_file.read().strip() + '\n\n' + finally: + description_file.close() + in_cfg_value = value + else: + continue + + if arg in CSV_FIELDS: + in_cfg_value = split_csv(in_cfg_value) + if arg in MULTI_FIELDS: + in_cfg_value = split_multiline(in_cfg_value) + elif arg in MAP_FIELDS: + in_cfg_map = {} + for i in split_multiline(in_cfg_value): + k, v = i.split('=') + in_cfg_map[k.strip()] = v.strip() + in_cfg_value = in_cfg_map + elif arg in BOOL_FIELDS: + # Provide some flexibility here... + if in_cfg_value.lower() in ('true', 't', '1', 'yes', 'y'): + in_cfg_value = True + else: + in_cfg_value = False + + if in_cfg_value: + if arg in ('install_requires', 'tests_require'): + # Replaces PEP345-style version specs with the sort expected by + # setuptools + in_cfg_value = [_VERSION_SPEC_RE.sub(r'\1\2', pred) + for pred in in_cfg_value] + if arg == 'install_requires': + # Split install_requires into package,env_marker tuples + # These will be re-assembled later + install_requires = [] + requirement_pattern = ( + r'(?P[^;]*);?(?P[^#]*?)(?:\s*#.*)?$') + for requirement in in_cfg_value: + m = re.match(requirement_pattern, requirement) + requirement_package = m.group('package').strip() + env_marker = m.group('env_marker').strip() + install_requires.append((requirement_package, env_marker)) + all_requirements[''] = install_requires + elif arg == 'package_dir': + in_cfg_value = {'': in_cfg_value} + elif arg in ('package_data', 'data_files'): + data_files = {} + firstline = True + prev = None + for line in in_cfg_value: + if '=' in line: + key, value = line.split('=', 1) + key, value = (key.strip(), value.strip()) + if key in data_files: + # Multiple duplicates of the same package name; + # this is for backwards compatibility of the old + # format prior to d2to1 0.2.6. + prev = data_files[key] + prev.extend(value.split()) + else: + prev = data_files[key.strip()] = value.split() + elif firstline: + raise errors.DistutilsOptionError( + 'malformed package_data first line %r (misses ' + '"=")' % line) + else: + prev.extend(line.strip().split()) + firstline = False + if arg == 'data_files': + # the data_files value is a pointlessly different structure + # from the package_data value + data_files = data_files.items() + in_cfg_value = data_files + elif arg == 'cmdclass': + cmdclass = {} + dist = st_dist.Distribution() + for cls_name in in_cfg_value: + cls = resolve_name(cls_name) + cmd = cls(dist) + cmdclass[cmd.get_command_name()] = cls + in_cfg_value = cmdclass + + kwargs[arg] = in_cfg_value + + # Transform requirements with embedded environment markers to + # setuptools' supported marker-per-requirement format. + # + # install_requires are treated as a special case of extras, before + # being put back in the expected place + # + # fred = + # foo:marker + # bar + # -> {'fred': ['bar'], 'fred:marker':['foo']} + + if 'extras' in config: + requirement_pattern = ( + r'(?P[^:]*):?(?P[^#]*?)(?:\s*#.*)?$') + extras = config['extras'] + # Add contents of test-requirements, if any, into an extra named + # 'test' if one does not already exist. + if 'test' not in extras: + from pbr import packaging + extras['test'] = "\n".join(packaging.parse_requirements( + packaging.TEST_REQUIREMENTS_FILES)).replace(';', ':') + + for extra in extras: + extra_requirements = [] + requirements = split_multiline(extras[extra]) + for requirement in requirements: + m = re.match(requirement_pattern, requirement) + extras_value = m.group('package').strip() + env_marker = m.group('env_marker') + extra_requirements.append((extras_value, env_marker)) + all_requirements[extra] = extra_requirements + + # Transform the full list of requirements into: + # - install_requires, for those that have no extra and no + # env_marker + # - named extras, for those with an extra name (which may include + # an env_marker) + # - and as a special case, install_requires with an env_marker are + # treated as named extras where the name is the empty string + + extras_require = {} + for req_group in all_requirements: + for requirement, env_marker in all_requirements[req_group]: + if env_marker: + extras_key = '%s:(%s)' % (req_group, env_marker) + # We do not want to poison wheel creation with locally + # evaluated markers. sdists always re-create the egg_info + # and as such do not need guarded, and pip will never call + # multiple setup.py commands at once. + if 'bdist_wheel' not in script_args: + try: + if pkg_resources.evaluate_marker('(%s)' % env_marker): + extras_key = req_group + except SyntaxError: + log.error( + "Marker evaluation failed, see the following " + "error. For more information see: " + "http://docs.openstack.org/" + "pbr/latest/user/using.html#environment-markers" + ) + raise + else: + extras_key = req_group + extras_require.setdefault(extras_key, []).append(requirement) + + kwargs['install_requires'] = extras_require.pop('', []) + kwargs['extras_require'] = extras_require + + return kwargs + + +def register_custom_compilers(config): + """Handle custom compilers. + + This has no real equivalent in distutils, where additional compilers could + only be added programmatically, so we have to hack it in somehow. + """ + + compilers = has_get_option(config, 'global', 'compilers') + if compilers: + compilers = split_multiline(compilers) + for compiler in compilers: + compiler = resolve_name(compiler) + + # In distutils2 compilers these class attributes exist; for + # distutils1 we just have to make something up + if hasattr(compiler, 'name'): + name = compiler.name + else: + name = compiler.__name__ + if hasattr(compiler, 'description'): + desc = compiler.description + else: + desc = 'custom compiler %s' % name + + module_name = compiler.__module__ + # Note; this *will* override built in compilers with the same name + # TODO(embray): Maybe display a warning about this? + cc = distutils.ccompiler.compiler_class + cc[name] = (module_name, compiler.__name__, desc) + + # HACK!!!! Distutils assumes all compiler modules are in the + # distutils package + sys.modules['distutils.' + module_name] = sys.modules[module_name] + + +def get_extension_modules(config): + """Handle extension modules""" + + EXTENSION_FIELDS = ("sources", + "include_dirs", + "define_macros", + "undef_macros", + "library_dirs", + "libraries", + "runtime_library_dirs", + "extra_objects", + "extra_compile_args", + "extra_link_args", + "export_symbols", + "swig_opts", + "depends") + + ext_modules = [] + for section in config: + if ':' in section: + labels = section.split(':', 1) + else: + # Backwards compatibility for old syntax; don't use this though + labels = section.split('=', 1) + labels = [l.strip() for l in labels] + if (len(labels) == 2) and (labels[0] == 'extension'): + ext_args = {} + for field in EXTENSION_FIELDS: + value = has_get_option(config, section, field) + # All extension module options besides name can have multiple + # values + if not value: + continue + value = split_multiline(value) + if field == 'define_macros': + macros = [] + for macro in value: + macro = macro.split('=', 1) + if len(macro) == 1: + macro = (macro[0].strip(), None) + else: + macro = (macro[0].strip(), macro[1].strip()) + macros.append(macro) + value = macros + ext_args[field] = value + if ext_args: + if 'name' not in ext_args: + ext_args['name'] = labels[1] + ext_modules.append(extension.Extension(ext_args.pop('name'), + **ext_args)) + return ext_modules + + +def get_entry_points(config): + """Process the [entry_points] section of setup.cfg. + + Processes setup.cfg to handle setuptools entry points. This is, of course, + not a standard feature of distutils2/packaging, but as there is not + currently a standard alternative in packaging, we provide support for them. + """ + + if 'entry_points' not in config: + return {} + + return dict((option, split_multiline(value)) + for option, value in config['entry_points'].items()) + + +def has_get_option(config, section, option): + if section in config and option in config[section]: + return config[section][option] + else: + return False + + +def split_multiline(value): + """Special behaviour when we have a multi line options""" + + value = [element for element in + (line.strip() for line in value.split('\n')) + if element and not element.startswith('#')] + return value + + +def split_csv(value): + """Special behaviour when we have a comma separated options""" + + value = [element for element in + (chunk.strip() for chunk in value.split(',')) + if element] + return value + + +# The following classes are used to hack Distribution.command_options a bit +class DefaultGetDict(defaultdict): + """Like defaultdict, but get() also sets and returns the default value.""" + + def get(self, key, default=None): + if default is None: + default = self.default_factory() + return super(DefaultGetDict, self).setdefault(key, default) diff --git a/libs/pbr/version.py b/libs/pbr/version.py new file mode 100644 index 00000000..5eb217af --- /dev/null +++ b/libs/pbr/version.py @@ -0,0 +1,483 @@ + +# Copyright 2012 OpenStack Foundation +# Copyright 2012-2013 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Utilities for consuming the version from pkg_resources. +""" + +import itertools +import operator +import sys + + +def _is_int(string): + try: + int(string) + return True + except ValueError: + return False + + +class SemanticVersion(object): + """A pure semantic version independent of serialisation. + + See the pbr doc 'semver' for details on the semantics. + """ + + def __init__( + self, major, minor=0, patch=0, prerelease_type=None, + prerelease=None, dev_count=None): + """Create a SemanticVersion. + + :param major: Major component of the version. + :param minor: Minor component of the version. Defaults to 0. + :param patch: Patch level component. Defaults to 0. + :param prerelease_type: What sort of prerelease version this is - + one of a(alpha), b(beta) or rc(release candidate). + :param prerelease: For prerelease versions, what number prerelease. + Defaults to 0. + :param dev_count: How many commits since the last release. + """ + self._major = major + self._minor = minor + self._patch = patch + self._prerelease_type = prerelease_type + self._prerelease = prerelease + if self._prerelease_type and not self._prerelease: + self._prerelease = 0 + self._dev_count = dev_count or 0 # Normalise 0 to None. + + def __eq__(self, other): + if not isinstance(other, SemanticVersion): + return False + return self.__dict__ == other.__dict__ + + def __hash__(self): + return sum(map(hash, self.__dict__.values())) + + def _sort_key(self): + """Return a key for sorting SemanticVersion's on.""" + # key things: + # - final is after rc's, so we make that a/b/rc/z + # - dev==None is after all other devs, so we use sys.maxsize there. + # - unqualified dev releases come before any pre-releases. + # So we do: + # (major, minor, patch) - gets the major grouping. + # (0|1) unqualified dev flag + # (a/b/rc/z) - release segment grouping + # pre-release level + # dev count, maxsize for releases. + rc_lookup = {'a': 'a', 'b': 'b', 'rc': 'rc', None: 'z'} + if self._dev_count and not self._prerelease_type: + uq_dev = 0 + else: + uq_dev = 1 + return ( + self._major, self._minor, self._patch, + uq_dev, + rc_lookup[self._prerelease_type], self._prerelease, + self._dev_count or sys.maxsize) + + def __lt__(self, other): + """Compare self and other, another Semantic Version.""" + # NB(lifeless) this could perhaps be rewritten as + # lt (tuple_of_one, tuple_of_other) with a single check for + # the typeerror corner cases - that would likely be faster + # if this ever becomes performance sensitive. + if not isinstance(other, SemanticVersion): + raise TypeError("ordering to non-SemanticVersion is undefined") + return self._sort_key() < other._sort_key() + + def __le__(self, other): + return self == other or self < other + + def __ge__(self, other): + return not self < other + + def __gt__(self, other): + return not self <= other + + def __ne__(self, other): + return not self == other + + def __repr__(self): + return "pbr.version.SemanticVersion(%s)" % self.release_string() + + @classmethod + def from_pip_string(klass, version_string): + """Create a SemanticVersion from a pip version string. + + This method will parse a version like 1.3.0 into a SemanticVersion. + + This method is responsible for accepting any version string that any + older version of pbr ever created. + + Therefore: versions like 1.3.0a1 versions are handled, parsed into a + canonical form and then output - resulting in 1.3.0.0a1. + Pre pbr-semver dev versions like 0.10.1.3.g83bef74 will be parsed but + output as 0.10.1.dev3.g83bef74. + + :raises ValueError: Never tagged versions sdisted by old pbr result in + just the git hash, e.g. '1234567' which poses a substantial problem + since they collide with the semver versions when all the digits are + numerals. Such versions will result in a ValueError being thrown if + any non-numeric digits are present. They are an exception to the + general case of accepting anything we ever output, since they were + never intended and would permanently mess up versions on PyPI if + ever released - we're treating that as a critical bug that we ever + made them and have stopped doing that. + """ + + try: + return klass._from_pip_string_unsafe(version_string) + except IndexError: + raise ValueError("Invalid version %r" % version_string) + + @classmethod + def _from_pip_string_unsafe(klass, version_string): + # Versions need to start numerically, ignore if not + version_string = version_string.lstrip('vV') + if not version_string[:1].isdigit(): + raise ValueError("Invalid version %r" % version_string) + input_components = version_string.split('.') + # decimals first (keep pre-release and dev/hashes to the right) + components = [c for c in input_components if c.isdigit()] + digit_len = len(components) + if digit_len == 0: + raise ValueError("Invalid version %r" % version_string) + elif digit_len < 3: + if (digit_len < len(input_components) and + input_components[digit_len][0].isdigit()): + # Handle X.YaZ - Y is a digit not a leadin to pre-release. + mixed_component = input_components[digit_len] + last_component = ''.join(itertools.takewhile( + lambda x: x.isdigit(), mixed_component)) + components.append(last_component) + input_components[digit_len:digit_len + 1] = [ + last_component, mixed_component[len(last_component):]] + digit_len += 1 + components.extend([0] * (3 - digit_len)) + components.extend(input_components[digit_len:]) + major = int(components[0]) + minor = int(components[1]) + dev_count = None + post_count = None + prerelease_type = None + prerelease = None + + def _parse_type(segment): + # Discard leading digits (the 0 in 0a1) + isdigit = operator.methodcaller('isdigit') + segment = ''.join(itertools.dropwhile(isdigit, segment)) + isalpha = operator.methodcaller('isalpha') + prerelease_type = ''.join(itertools.takewhile(isalpha, segment)) + prerelease = segment[len(prerelease_type)::] + return prerelease_type, int(prerelease) + if _is_int(components[2]): + patch = int(components[2]) + else: + # legacy version e.g. 1.2.0a1 (canonical is 1.2.0.0a1) + # or 1.2.dev4.g1234 or 1.2.b4 + patch = 0 + components[2:2] = [0] + remainder = components[3:] + remainder_starts_with_int = False + try: + if remainder and int(remainder[0]): + remainder_starts_with_int = True + except ValueError: + pass + if remainder_starts_with_int: + # old dev format - 0.1.2.3.g1234 + dev_count = int(remainder[0]) + else: + if remainder and (remainder[0][0] == '0' or + remainder[0][0] in ('a', 'b', 'r')): + # Current RC/beta layout + prerelease_type, prerelease = _parse_type(remainder[0]) + remainder = remainder[1:] + while remainder: + component = remainder[0] + if component.startswith('dev'): + dev_count = int(component[3:]) + elif component.startswith('post'): + dev_count = None + post_count = int(component[4:]) + else: + raise ValueError( + 'Unknown remainder %r in %r' + % (remainder, version_string)) + remainder = remainder[1:] + result = SemanticVersion( + major, minor, patch, prerelease_type=prerelease_type, + prerelease=prerelease, dev_count=dev_count) + if post_count: + if dev_count: + raise ValueError( + 'Cannot combine postN and devN - no mapping in %r' + % (version_string,)) + result = result.increment().to_dev(post_count) + return result + + def brief_string(self): + """Return the short version minus any alpha/beta tags.""" + return "%s.%s.%s" % (self._major, self._minor, self._patch) + + def debian_string(self): + """Return the version number to use when building a debian package. + + This translates the PEP440/semver precedence rules into Debian version + sorting operators. + """ + return self._long_version("~") + + def decrement(self): + """Return a decremented SemanticVersion. + + Decrementing versions doesn't make a lot of sense - this method only + exists to support rendering of pre-release versions strings into + serialisations (such as rpm) with no sort-before operator. + + The 9999 magic version component is from the spec on this - pbr-semver. + + :return: A new SemanticVersion object. + """ + if self._patch: + new_patch = self._patch - 1 + new_minor = self._minor + new_major = self._major + else: + new_patch = 9999 + if self._minor: + new_minor = self._minor - 1 + new_major = self._major + else: + new_minor = 9999 + if self._major: + new_major = self._major - 1 + else: + new_major = 0 + return SemanticVersion( + new_major, new_minor, new_patch) + + def increment(self, minor=False, major=False): + """Return an incremented SemanticVersion. + + The default behaviour is to perform a patch level increment. When + incrementing a prerelease version, the patch level is not changed + - the prerelease serial is changed (e.g. beta 0 -> beta 1). + + Incrementing non-pre-release versions will not introduce pre-release + versions - except when doing a patch incremental to a pre-release + version the new version will only consist of major/minor/patch. + + :param minor: Increment the minor version. + :param major: Increment the major version. + :return: A new SemanticVersion object. + """ + if self._prerelease_type: + new_prerelease_type = self._prerelease_type + new_prerelease = self._prerelease + 1 + new_patch = self._patch + else: + new_prerelease_type = None + new_prerelease = None + new_patch = self._patch + 1 + if minor: + new_minor = self._minor + 1 + new_patch = 0 + new_prerelease_type = None + new_prerelease = None + else: + new_minor = self._minor + if major: + new_major = self._major + 1 + new_minor = 0 + new_patch = 0 + new_prerelease_type = None + new_prerelease = None + else: + new_major = self._major + return SemanticVersion( + new_major, new_minor, new_patch, + new_prerelease_type, new_prerelease) + + def _long_version(self, pre_separator, rc_marker=""): + """Construct a long string version of this semver. + + :param pre_separator: What separator to use between components + that sort before rather than after. If None, use . and lower the + version number of the component to preserve sorting. (Used for + rpm support) + """ + if ((self._prerelease_type or self._dev_count) + and pre_separator is None): + segments = [self.decrement().brief_string()] + pre_separator = "." + else: + segments = [self.brief_string()] + if self._prerelease_type: + segments.append( + "%s%s%s%s" % (pre_separator, rc_marker, self._prerelease_type, + self._prerelease)) + if self._dev_count: + if not self._prerelease_type: + segments.append(pre_separator) + else: + segments.append('.') + segments.append('dev') + segments.append(self._dev_count) + return "".join(str(s) for s in segments) + + def release_string(self): + """Return the full version of the package. + + This including suffixes indicating VCS status. + """ + return self._long_version(".", "0") + + def rpm_string(self): + """Return the version number to use when building an RPM package. + + This translates the PEP440/semver precedence rules into RPM version + sorting operators. Because RPM has no sort-before operator (such as the + ~ operator in dpkg), we show all prerelease versions as being versions + of the release before. + """ + return self._long_version(None) + + def to_dev(self, dev_count): + """Return a development version of this semver. + + :param dev_count: The number of commits since the last release. + """ + return SemanticVersion( + self._major, self._minor, self._patch, self._prerelease_type, + self._prerelease, dev_count=dev_count) + + def version_tuple(self): + """Present the version as a version_info tuple. + + For documentation on version_info tuples see the Python + documentation for sys.version_info. + + Since semver and PEP-440 represent overlapping but not subsets of + versions, we have to have some heuristic / mapping rules, and have + extended the releaselevel field to have alphadev, betadev and + candidatedev values. When they are present the dev count is used + to provide the serial. + - a/b/rc take precedence. + - if there is no pre-release version the dev version is used. + - serial is taken from the dev/a/b/c component. + - final non-dev versions never get serials. + """ + segments = [self._major, self._minor, self._patch] + if self._prerelease_type: + type_map = {('a', False): 'alpha', + ('b', False): 'beta', + ('rc', False): 'candidate', + ('a', True): 'alphadev', + ('b', True): 'betadev', + ('rc', True): 'candidatedev', + } + segments.append( + type_map[(self._prerelease_type, bool(self._dev_count))]) + segments.append(self._dev_count or self._prerelease) + elif self._dev_count: + segments.append('dev') + segments.append(self._dev_count - 1) + else: + segments.append('final') + segments.append(0) + return tuple(segments) + + +class VersionInfo(object): + + def __init__(self, package): + """Object that understands versioning for a package + + :param package: name of the python package, such as glance, or + python-glanceclient + """ + self.package = package + self.version = None + self._cached_version = None + self._semantic = None + + def __str__(self): + """Make the VersionInfo object behave like a string.""" + return self.version_string() + + def __repr__(self): + """Include the name.""" + return "pbr.version.VersionInfo(%s:%s)" % ( + self.package, self.version_string()) + + def _get_version_from_pkg_resources(self): + """Obtain a version from pkg_resources or setup-time logic if missing. + + This will try to get the version of the package from the pkg_resources + record associated with the package, and if there is no such record + falls back to the logic sdist would use. + """ + # Lazy import because pkg_resources is costly to import so defer until + # we absolutely need it. + import pkg_resources + try: + requirement = pkg_resources.Requirement.parse(self.package) + provider = pkg_resources.get_provider(requirement) + result_string = provider.version + except pkg_resources.DistributionNotFound: + # The most likely cause for this is running tests in a tree + # produced from a tarball where the package itself has not been + # installed into anything. Revert to setup-time logic. + from pbr import packaging + result_string = packaging.get_version(self.package) + return SemanticVersion.from_pip_string(result_string) + + def release_string(self): + """Return the full version of the package. + + This including suffixes indicating VCS status. + """ + return self.semantic_version().release_string() + + def semantic_version(self): + """Return the SemanticVersion object for this version.""" + if self._semantic is None: + self._semantic = self._get_version_from_pkg_resources() + return self._semantic + + def version_string(self): + """Return the short version minus any alpha/beta tags.""" + return self.semantic_version().brief_string() + + # Compatibility functions + canonical_version_string = version_string + version_string_with_vcs = release_string + + def cached_version_string(self, prefix=""): + """Return a cached version string. + + This will return a cached version string if one is already cached, + irrespective of prefix. If none is cached, one will be created with + prefix and then cached and returned. + """ + if not self._cached_version: + self._cached_version = "%s%s" % (prefix, + self.version_string()) + return self._cached_version diff --git a/libs/pytz/__init__.py b/libs/pytz/__init__.py new file mode 100644 index 00000000..6e923173 --- /dev/null +++ b/libs/pytz/__init__.py @@ -0,0 +1,1527 @@ +''' +datetime.tzinfo timezone definitions generated from the +Olson timezone database: + + ftp://elsie.nci.nih.gov/pub/tz*.tar.gz + +See the datetime section of the Python Library Reference for information +on how to use these modules. +''' + +import sys +import datetime +import os.path + +from pytz.exceptions import AmbiguousTimeError +from pytz.exceptions import InvalidTimeError +from pytz.exceptions import NonExistentTimeError +from pytz.exceptions import UnknownTimeZoneError +from pytz.lazy import LazyDict, LazyList, LazySet +from pytz.tzinfo import unpickler, BaseTzInfo +from pytz.tzfile import build_tzinfo + + +# The IANA (nee Olson) database is updated several times a year. +OLSON_VERSION = '2018g' +VERSION = '2018.7' # pip compatible version number. +__version__ = VERSION + +OLSEN_VERSION = OLSON_VERSION # Old releases had this misspelling + +__all__ = [ + 'timezone', 'utc', 'country_timezones', 'country_names', + 'AmbiguousTimeError', 'InvalidTimeError', + 'NonExistentTimeError', 'UnknownTimeZoneError', + 'all_timezones', 'all_timezones_set', + 'common_timezones', 'common_timezones_set', + 'BaseTzInfo', +] + + +if sys.version_info[0] > 2: # Python 3.x + + # Python 3.x doesn't have unicode(), making writing code + # for Python 2.3 and Python 3.x a pain. + unicode = str + + def ascii(s): + r""" + >>> ascii('Hello') + 'Hello' + >>> ascii('\N{TRADE MARK SIGN}') #doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + UnicodeEncodeError: ... + """ + if type(s) == bytes: + s = s.decode('ASCII') + else: + s.encode('ASCII') # Raise an exception if not ASCII + return s # But the string - not a byte string. + +else: # Python 2.x + + def ascii(s): + r""" + >>> ascii('Hello') + 'Hello' + >>> ascii(u'Hello') + 'Hello' + >>> ascii(u'\N{TRADE MARK SIGN}') #doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + UnicodeEncodeError: ... + """ + return s.encode('ASCII') + + +def open_resource(name): + """Open a resource from the zoneinfo subdir for reading. + + Uses the pkg_resources module if available and no standard file + found at the calculated location. + + It is possible to specify different location for zoneinfo + subdir by using the PYTZ_TZDATADIR environment variable. + """ + name_parts = name.lstrip('/').split('/') + for part in name_parts: + if part == os.path.pardir or os.path.sep in part: + raise ValueError('Bad path segment: %r' % part) + zoneinfo_dir = os.environ.get('PYTZ_TZDATADIR', None) + if zoneinfo_dir is not None: + filename = os.path.join(zoneinfo_dir, *name_parts) + else: + filename = os.path.join(os.path.dirname(__file__), + 'zoneinfo', *name_parts) + if not os.path.exists(filename): + # http://bugs.launchpad.net/bugs/383171 - we avoid using this + # unless absolutely necessary to help when a broken version of + # pkg_resources is installed. + try: + from pkg_resources import resource_stream + except ImportError: + resource_stream = None + + if resource_stream is not None: + return resource_stream(__name__, 'zoneinfo/' + name) + return open(filename, 'rb') + + +def resource_exists(name): + """Return true if the given resource exists""" + try: + open_resource(name).close() + return True + except IOError: + return False + + +_tzinfo_cache = {} + + +def timezone(zone): + r''' Return a datetime.tzinfo implementation for the given timezone + + >>> from datetime import datetime, timedelta + >>> utc = timezone('UTC') + >>> eastern = timezone('US/Eastern') + >>> eastern.zone + 'US/Eastern' + >>> timezone(unicode('US/Eastern')) is eastern + True + >>> utc_dt = datetime(2002, 10, 27, 6, 0, 0, tzinfo=utc) + >>> loc_dt = utc_dt.astimezone(eastern) + >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)' + >>> loc_dt.strftime(fmt) + '2002-10-27 01:00:00 EST (-0500)' + >>> (loc_dt - timedelta(minutes=10)).strftime(fmt) + '2002-10-27 00:50:00 EST (-0500)' + >>> eastern.normalize(loc_dt - timedelta(minutes=10)).strftime(fmt) + '2002-10-27 01:50:00 EDT (-0400)' + >>> (loc_dt + timedelta(minutes=10)).strftime(fmt) + '2002-10-27 01:10:00 EST (-0500)' + + Raises UnknownTimeZoneError if passed an unknown zone. + + >>> try: + ... timezone('Asia/Shangri-La') + ... except UnknownTimeZoneError: + ... print('Unknown') + Unknown + + >>> try: + ... timezone(unicode('\N{TRADE MARK SIGN}')) + ... except UnknownTimeZoneError: + ... print('Unknown') + Unknown + + ''' + if zone.upper() == 'UTC': + return utc + + try: + zone = ascii(zone) + except UnicodeEncodeError: + # All valid timezones are ASCII + raise UnknownTimeZoneError(zone) + + zone = _unmunge_zone(zone) + if zone not in _tzinfo_cache: + if zone in all_timezones_set: + fp = open_resource(zone) + try: + _tzinfo_cache[zone] = build_tzinfo(zone, fp) + finally: + fp.close() + else: + raise UnknownTimeZoneError(zone) + + return _tzinfo_cache[zone] + + +def _unmunge_zone(zone): + """Undo the time zone name munging done by older versions of pytz.""" + return zone.replace('_plus_', '+').replace('_minus_', '-') + + +ZERO = datetime.timedelta(0) +HOUR = datetime.timedelta(hours=1) + + +class UTC(BaseTzInfo): + """UTC + + Optimized UTC implementation. It unpickles using the single module global + instance defined beneath this class declaration. + """ + zone = "UTC" + + _utcoffset = ZERO + _dst = ZERO + _tzname = zone + + def fromutc(self, dt): + if dt.tzinfo is None: + return self.localize(dt) + return super(utc.__class__, self).fromutc(dt) + + def utcoffset(self, dt): + return ZERO + + def tzname(self, dt): + return "UTC" + + def dst(self, dt): + return ZERO + + def __reduce__(self): + return _UTC, () + + def localize(self, dt, is_dst=False): + '''Convert naive time to local time''' + if dt.tzinfo is not None: + raise ValueError('Not naive datetime (tzinfo is already set)') + return dt.replace(tzinfo=self) + + def normalize(self, dt, is_dst=False): + '''Correct the timezone information on the given datetime''' + if dt.tzinfo is self: + return dt + if dt.tzinfo is None: + raise ValueError('Naive time - no tzinfo set') + return dt.astimezone(self) + + def __repr__(self): + return "" + + def __str__(self): + return "UTC" + + +UTC = utc = UTC() # UTC is a singleton + + +def _UTC(): + """Factory function for utc unpickling. + + Makes sure that unpickling a utc instance always returns the same + module global. + + These examples belong in the UTC class above, but it is obscured; or in + the README.txt, but we are not depending on Python 2.4 so integrating + the README.txt examples with the unit tests is not trivial. + + >>> import datetime, pickle + >>> dt = datetime.datetime(2005, 3, 1, 14, 13, 21, tzinfo=utc) + >>> naive = dt.replace(tzinfo=None) + >>> p = pickle.dumps(dt, 1) + >>> naive_p = pickle.dumps(naive, 1) + >>> len(p) - len(naive_p) + 17 + >>> new = pickle.loads(p) + >>> new == dt + True + >>> new is dt + False + >>> new.tzinfo is dt.tzinfo + True + >>> utc is UTC is timezone('UTC') + True + >>> utc is timezone('GMT') + False + """ + return utc +_UTC.__safe_for_unpickling__ = True + + +def _p(*args): + """Factory function for unpickling pytz tzinfo instances. + + Just a wrapper around tzinfo.unpickler to save a few bytes in each pickle + by shortening the path. + """ + return unpickler(*args) +_p.__safe_for_unpickling__ = True + + +class _CountryTimezoneDict(LazyDict): + """Map ISO 3166 country code to a list of timezone names commonly used + in that country. + + iso3166_code is the two letter code used to identify the country. + + >>> def print_list(list_of_strings): + ... 'We use a helper so doctests work under Python 2.3 -> 3.x' + ... for s in list_of_strings: + ... print(s) + + >>> print_list(country_timezones['nz']) + Pacific/Auckland + Pacific/Chatham + >>> print_list(country_timezones['ch']) + Europe/Zurich + >>> print_list(country_timezones['CH']) + Europe/Zurich + >>> print_list(country_timezones[unicode('ch')]) + Europe/Zurich + >>> print_list(country_timezones['XXX']) + Traceback (most recent call last): + ... + KeyError: 'XXX' + + Previously, this information was exposed as a function rather than a + dictionary. This is still supported:: + + >>> print_list(country_timezones('nz')) + Pacific/Auckland + Pacific/Chatham + """ + def __call__(self, iso3166_code): + """Backwards compatibility.""" + return self[iso3166_code] + + def _fill(self): + data = {} + zone_tab = open_resource('zone.tab') + try: + for line in zone_tab: + line = line.decode('UTF-8') + if line.startswith('#'): + continue + code, coordinates, zone = line.split(None, 4)[:3] + if zone not in all_timezones_set: + continue + try: + data[code].append(zone) + except KeyError: + data[code] = [zone] + self.data = data + finally: + zone_tab.close() + +country_timezones = _CountryTimezoneDict() + + +class _CountryNameDict(LazyDict): + '''Dictionary proving ISO3166 code -> English name. + + >>> print(country_names['au']) + Australia + ''' + def _fill(self): + data = {} + zone_tab = open_resource('iso3166.tab') + try: + for line in zone_tab.readlines(): + line = line.decode('UTF-8') + if line.startswith('#'): + continue + code, name = line.split(None, 1) + data[code] = name.strip() + self.data = data + finally: + zone_tab.close() + +country_names = _CountryNameDict() + + +# Time-zone info based solely on fixed offsets + +class _FixedOffset(datetime.tzinfo): + + zone = None # to match the standard pytz API + + def __init__(self, minutes): + if abs(minutes) >= 1440: + raise ValueError("absolute offset is too large", minutes) + self._minutes = minutes + self._offset = datetime.timedelta(minutes=minutes) + + def utcoffset(self, dt): + return self._offset + + def __reduce__(self): + return FixedOffset, (self._minutes, ) + + def dst(self, dt): + return ZERO + + def tzname(self, dt): + return None + + def __repr__(self): + return 'pytz.FixedOffset(%d)' % self._minutes + + def localize(self, dt, is_dst=False): + '''Convert naive time to local time''' + if dt.tzinfo is not None: + raise ValueError('Not naive datetime (tzinfo is already set)') + return dt.replace(tzinfo=self) + + def normalize(self, dt, is_dst=False): + '''Correct the timezone information on the given datetime''' + if dt.tzinfo is self: + return dt + if dt.tzinfo is None: + raise ValueError('Naive time - no tzinfo set') + return dt.astimezone(self) + + +def FixedOffset(offset, _tzinfos={}): + """return a fixed-offset timezone based off a number of minutes. + + >>> one = FixedOffset(-330) + >>> one + pytz.FixedOffset(-330) + >>> str(one.utcoffset(datetime.datetime.now())) + '-1 day, 18:30:00' + >>> str(one.dst(datetime.datetime.now())) + '0:00:00' + + >>> two = FixedOffset(1380) + >>> two + pytz.FixedOffset(1380) + >>> str(two.utcoffset(datetime.datetime.now())) + '23:00:00' + >>> str(two.dst(datetime.datetime.now())) + '0:00:00' + + The datetime.timedelta must be between the range of -1 and 1 day, + non-inclusive. + + >>> FixedOffset(1440) + Traceback (most recent call last): + ... + ValueError: ('absolute offset is too large', 1440) + + >>> FixedOffset(-1440) + Traceback (most recent call last): + ... + ValueError: ('absolute offset is too large', -1440) + + An offset of 0 is special-cased to return UTC. + + >>> FixedOffset(0) is UTC + True + + There should always be only one instance of a FixedOffset per timedelta. + This should be true for multiple creation calls. + + >>> FixedOffset(-330) is one + True + >>> FixedOffset(1380) is two + True + + It should also be true for pickling. + + >>> import pickle + >>> pickle.loads(pickle.dumps(one)) is one + True + >>> pickle.loads(pickle.dumps(two)) is two + True + """ + if offset == 0: + return UTC + + info = _tzinfos.get(offset) + if info is None: + # We haven't seen this one before. we need to save it. + + # Use setdefault to avoid a race condition and make sure we have + # only one + info = _tzinfos.setdefault(offset, _FixedOffset(offset)) + + return info + +FixedOffset.__safe_for_unpickling__ = True + + +def _test(): + import doctest + sys.path.insert(0, os.pardir) + import pytz + return doctest.testmod(pytz) + +if __name__ == '__main__': + _test() +all_timezones = \ +['Africa/Abidjan', + 'Africa/Accra', + 'Africa/Addis_Ababa', + 'Africa/Algiers', + 'Africa/Asmara', + 'Africa/Asmera', + 'Africa/Bamako', + 'Africa/Bangui', + 'Africa/Banjul', + 'Africa/Bissau', + 'Africa/Blantyre', + 'Africa/Brazzaville', + 'Africa/Bujumbura', + 'Africa/Cairo', + 'Africa/Casablanca', + 'Africa/Ceuta', + 'Africa/Conakry', + 'Africa/Dakar', + 'Africa/Dar_es_Salaam', + 'Africa/Djibouti', + 'Africa/Douala', + 'Africa/El_Aaiun', + 'Africa/Freetown', + 'Africa/Gaborone', + 'Africa/Harare', + 'Africa/Johannesburg', + 'Africa/Juba', + 'Africa/Kampala', + 'Africa/Khartoum', + 'Africa/Kigali', + 'Africa/Kinshasa', + 'Africa/Lagos', + 'Africa/Libreville', + 'Africa/Lome', + 'Africa/Luanda', + 'Africa/Lubumbashi', + 'Africa/Lusaka', + 'Africa/Malabo', + 'Africa/Maputo', + 'Africa/Maseru', + 'Africa/Mbabane', + 'Africa/Mogadishu', + 'Africa/Monrovia', + 'Africa/Nairobi', + 'Africa/Ndjamena', + 'Africa/Niamey', + 'Africa/Nouakchott', + 'Africa/Ouagadougou', + 'Africa/Porto-Novo', + 'Africa/Sao_Tome', + 'Africa/Timbuktu', + 'Africa/Tripoli', + 'Africa/Tunis', + 'Africa/Windhoek', + 'America/Adak', + 'America/Anchorage', + 'America/Anguilla', + 'America/Antigua', + 'America/Araguaina', + 'America/Argentina/Buenos_Aires', + 'America/Argentina/Catamarca', + 'America/Argentina/ComodRivadavia', + 'America/Argentina/Cordoba', + 'America/Argentina/Jujuy', + 'America/Argentina/La_Rioja', + 'America/Argentina/Mendoza', + 'America/Argentina/Rio_Gallegos', + 'America/Argentina/Salta', + 'America/Argentina/San_Juan', + 'America/Argentina/San_Luis', + 'America/Argentina/Tucuman', + 'America/Argentina/Ushuaia', + 'America/Aruba', + 'America/Asuncion', + 'America/Atikokan', + 'America/Atka', + 'America/Bahia', + 'America/Bahia_Banderas', + 'America/Barbados', + 'America/Belem', + 'America/Belize', + 'America/Blanc-Sablon', + 'America/Boa_Vista', + 'America/Bogota', + 'America/Boise', + 'America/Buenos_Aires', + 'America/Cambridge_Bay', + 'America/Campo_Grande', + 'America/Cancun', + 'America/Caracas', + 'America/Catamarca', + 'America/Cayenne', + 'America/Cayman', + 'America/Chicago', + 'America/Chihuahua', + 'America/Coral_Harbour', + 'America/Cordoba', + 'America/Costa_Rica', + 'America/Creston', + 'America/Cuiaba', + 'America/Curacao', + 'America/Danmarkshavn', + 'America/Dawson', + 'America/Dawson_Creek', + 'America/Denver', + 'America/Detroit', + 'America/Dominica', + 'America/Edmonton', + 'America/Eirunepe', + 'America/El_Salvador', + 'America/Ensenada', + 'America/Fort_Nelson', + 'America/Fort_Wayne', + 'America/Fortaleza', + 'America/Glace_Bay', + 'America/Godthab', + 'America/Goose_Bay', + 'America/Grand_Turk', + 'America/Grenada', + 'America/Guadeloupe', + 'America/Guatemala', + 'America/Guayaquil', + 'America/Guyana', + 'America/Halifax', + 'America/Havana', + 'America/Hermosillo', + 'America/Indiana/Indianapolis', + 'America/Indiana/Knox', + 'America/Indiana/Marengo', + 'America/Indiana/Petersburg', + 'America/Indiana/Tell_City', + 'America/Indiana/Vevay', + 'America/Indiana/Vincennes', + 'America/Indiana/Winamac', + 'America/Indianapolis', + 'America/Inuvik', + 'America/Iqaluit', + 'America/Jamaica', + 'America/Jujuy', + 'America/Juneau', + 'America/Kentucky/Louisville', + 'America/Kentucky/Monticello', + 'America/Knox_IN', + 'America/Kralendijk', + 'America/La_Paz', + 'America/Lima', + 'America/Los_Angeles', + 'America/Louisville', + 'America/Lower_Princes', + 'America/Maceio', + 'America/Managua', + 'America/Manaus', + 'America/Marigot', + 'America/Martinique', + 'America/Matamoros', + 'America/Mazatlan', + 'America/Mendoza', + 'America/Menominee', + 'America/Merida', + 'America/Metlakatla', + 'America/Mexico_City', + 'America/Miquelon', + 'America/Moncton', + 'America/Monterrey', + 'America/Montevideo', + 'America/Montreal', + 'America/Montserrat', + 'America/Nassau', + 'America/New_York', + 'America/Nipigon', + 'America/Nome', + 'America/Noronha', + 'America/North_Dakota/Beulah', + 'America/North_Dakota/Center', + 'America/North_Dakota/New_Salem', + 'America/Ojinaga', + 'America/Panama', + 'America/Pangnirtung', + 'America/Paramaribo', + 'America/Phoenix', + 'America/Port-au-Prince', + 'America/Port_of_Spain', + 'America/Porto_Acre', + 'America/Porto_Velho', + 'America/Puerto_Rico', + 'America/Punta_Arenas', + 'America/Rainy_River', + 'America/Rankin_Inlet', + 'America/Recife', + 'America/Regina', + 'America/Resolute', + 'America/Rio_Branco', + 'America/Rosario', + 'America/Santa_Isabel', + 'America/Santarem', + 'America/Santiago', + 'America/Santo_Domingo', + 'America/Sao_Paulo', + 'America/Scoresbysund', + 'America/Shiprock', + 'America/Sitka', + 'America/St_Barthelemy', + 'America/St_Johns', + 'America/St_Kitts', + 'America/St_Lucia', + 'America/St_Thomas', + 'America/St_Vincent', + 'America/Swift_Current', + 'America/Tegucigalpa', + 'America/Thule', + 'America/Thunder_Bay', + 'America/Tijuana', + 'America/Toronto', + 'America/Tortola', + 'America/Vancouver', + 'America/Virgin', + 'America/Whitehorse', + 'America/Winnipeg', + 'America/Yakutat', + 'America/Yellowknife', + 'Antarctica/Casey', + 'Antarctica/Davis', + 'Antarctica/DumontDUrville', + 'Antarctica/Macquarie', + 'Antarctica/Mawson', + 'Antarctica/McMurdo', + 'Antarctica/Palmer', + 'Antarctica/Rothera', + 'Antarctica/South_Pole', + 'Antarctica/Syowa', + 'Antarctica/Troll', + 'Antarctica/Vostok', + 'Arctic/Longyearbyen', + 'Asia/Aden', + 'Asia/Almaty', + 'Asia/Amman', + 'Asia/Anadyr', + 'Asia/Aqtau', + 'Asia/Aqtobe', + 'Asia/Ashgabat', + 'Asia/Ashkhabad', + 'Asia/Atyrau', + 'Asia/Baghdad', + 'Asia/Bahrain', + 'Asia/Baku', + 'Asia/Bangkok', + 'Asia/Barnaul', + 'Asia/Beirut', + 'Asia/Bishkek', + 'Asia/Brunei', + 'Asia/Calcutta', + 'Asia/Chita', + 'Asia/Choibalsan', + 'Asia/Chongqing', + 'Asia/Chungking', + 'Asia/Colombo', + 'Asia/Dacca', + 'Asia/Damascus', + 'Asia/Dhaka', + 'Asia/Dili', + 'Asia/Dubai', + 'Asia/Dushanbe', + 'Asia/Famagusta', + 'Asia/Gaza', + 'Asia/Harbin', + 'Asia/Hebron', + 'Asia/Ho_Chi_Minh', + 'Asia/Hong_Kong', + 'Asia/Hovd', + 'Asia/Irkutsk', + 'Asia/Istanbul', + 'Asia/Jakarta', + 'Asia/Jayapura', + 'Asia/Jerusalem', + 'Asia/Kabul', + 'Asia/Kamchatka', + 'Asia/Karachi', + 'Asia/Kashgar', + 'Asia/Kathmandu', + 'Asia/Katmandu', + 'Asia/Khandyga', + 'Asia/Kolkata', + 'Asia/Krasnoyarsk', + 'Asia/Kuala_Lumpur', + 'Asia/Kuching', + 'Asia/Kuwait', + 'Asia/Macao', + 'Asia/Macau', + 'Asia/Magadan', + 'Asia/Makassar', + 'Asia/Manila', + 'Asia/Muscat', + 'Asia/Nicosia', + 'Asia/Novokuznetsk', + 'Asia/Novosibirsk', + 'Asia/Omsk', + 'Asia/Oral', + 'Asia/Phnom_Penh', + 'Asia/Pontianak', + 'Asia/Pyongyang', + 'Asia/Qatar', + 'Asia/Qyzylorda', + 'Asia/Rangoon', + 'Asia/Riyadh', + 'Asia/Saigon', + 'Asia/Sakhalin', + 'Asia/Samarkand', + 'Asia/Seoul', + 'Asia/Shanghai', + 'Asia/Singapore', + 'Asia/Srednekolymsk', + 'Asia/Taipei', + 'Asia/Tashkent', + 'Asia/Tbilisi', + 'Asia/Tehran', + 'Asia/Tel_Aviv', + 'Asia/Thimbu', + 'Asia/Thimphu', + 'Asia/Tokyo', + 'Asia/Tomsk', + 'Asia/Ujung_Pandang', + 'Asia/Ulaanbaatar', + 'Asia/Ulan_Bator', + 'Asia/Urumqi', + 'Asia/Ust-Nera', + 'Asia/Vientiane', + 'Asia/Vladivostok', + 'Asia/Yakutsk', + 'Asia/Yangon', + 'Asia/Yekaterinburg', + 'Asia/Yerevan', + 'Atlantic/Azores', + 'Atlantic/Bermuda', + 'Atlantic/Canary', + 'Atlantic/Cape_Verde', + 'Atlantic/Faeroe', + 'Atlantic/Faroe', + 'Atlantic/Jan_Mayen', + 'Atlantic/Madeira', + 'Atlantic/Reykjavik', + 'Atlantic/South_Georgia', + 'Atlantic/St_Helena', + 'Atlantic/Stanley', + 'Australia/ACT', + 'Australia/Adelaide', + 'Australia/Brisbane', + 'Australia/Broken_Hill', + 'Australia/Canberra', + 'Australia/Currie', + 'Australia/Darwin', + 'Australia/Eucla', + 'Australia/Hobart', + 'Australia/LHI', + 'Australia/Lindeman', + 'Australia/Lord_Howe', + 'Australia/Melbourne', + 'Australia/NSW', + 'Australia/North', + 'Australia/Perth', + 'Australia/Queensland', + 'Australia/South', + 'Australia/Sydney', + 'Australia/Tasmania', + 'Australia/Victoria', + 'Australia/West', + 'Australia/Yancowinna', + 'Brazil/Acre', + 'Brazil/DeNoronha', + 'Brazil/East', + 'Brazil/West', + 'CET', + 'CST6CDT', + 'Canada/Atlantic', + 'Canada/Central', + 'Canada/Eastern', + 'Canada/Mountain', + 'Canada/Newfoundland', + 'Canada/Pacific', + 'Canada/Saskatchewan', + 'Canada/Yukon', + 'Chile/Continental', + 'Chile/EasterIsland', + 'Cuba', + 'EET', + 'EST', + 'EST5EDT', + 'Egypt', + 'Eire', + 'Etc/GMT', + 'Etc/GMT+0', + 'Etc/GMT+1', + 'Etc/GMT+10', + 'Etc/GMT+11', + 'Etc/GMT+12', + 'Etc/GMT+2', + 'Etc/GMT+3', + 'Etc/GMT+4', + 'Etc/GMT+5', + 'Etc/GMT+6', + 'Etc/GMT+7', + 'Etc/GMT+8', + 'Etc/GMT+9', + 'Etc/GMT-0', + 'Etc/GMT-1', + 'Etc/GMT-10', + 'Etc/GMT-11', + 'Etc/GMT-12', + 'Etc/GMT-13', + 'Etc/GMT-14', + 'Etc/GMT-2', + 'Etc/GMT-3', + 'Etc/GMT-4', + 'Etc/GMT-5', + 'Etc/GMT-6', + 'Etc/GMT-7', + 'Etc/GMT-8', + 'Etc/GMT-9', + 'Etc/GMT0', + 'Etc/Greenwich', + 'Etc/UCT', + 'Etc/UTC', + 'Etc/Universal', + 'Etc/Zulu', + 'Europe/Amsterdam', + 'Europe/Andorra', + 'Europe/Astrakhan', + 'Europe/Athens', + 'Europe/Belfast', + 'Europe/Belgrade', + 'Europe/Berlin', + 'Europe/Bratislava', + 'Europe/Brussels', + 'Europe/Bucharest', + 'Europe/Budapest', + 'Europe/Busingen', + 'Europe/Chisinau', + 'Europe/Copenhagen', + 'Europe/Dublin', + 'Europe/Gibraltar', + 'Europe/Guernsey', + 'Europe/Helsinki', + 'Europe/Isle_of_Man', + 'Europe/Istanbul', + 'Europe/Jersey', + 'Europe/Kaliningrad', + 'Europe/Kiev', + 'Europe/Kirov', + 'Europe/Lisbon', + 'Europe/Ljubljana', + 'Europe/London', + 'Europe/Luxembourg', + 'Europe/Madrid', + 'Europe/Malta', + 'Europe/Mariehamn', + 'Europe/Minsk', + 'Europe/Monaco', + 'Europe/Moscow', + 'Europe/Nicosia', + 'Europe/Oslo', + 'Europe/Paris', + 'Europe/Podgorica', + 'Europe/Prague', + 'Europe/Riga', + 'Europe/Rome', + 'Europe/Samara', + 'Europe/San_Marino', + 'Europe/Sarajevo', + 'Europe/Saratov', + 'Europe/Simferopol', + 'Europe/Skopje', + 'Europe/Sofia', + 'Europe/Stockholm', + 'Europe/Tallinn', + 'Europe/Tirane', + 'Europe/Tiraspol', + 'Europe/Ulyanovsk', + 'Europe/Uzhgorod', + 'Europe/Vaduz', + 'Europe/Vatican', + 'Europe/Vienna', + 'Europe/Vilnius', + 'Europe/Volgograd', + 'Europe/Warsaw', + 'Europe/Zagreb', + 'Europe/Zaporozhye', + 'Europe/Zurich', + 'GB', + 'GB-Eire', + 'GMT', + 'GMT+0', + 'GMT-0', + 'GMT0', + 'Greenwich', + 'HST', + 'Hongkong', + 'Iceland', + 'Indian/Antananarivo', + 'Indian/Chagos', + 'Indian/Christmas', + 'Indian/Cocos', + 'Indian/Comoro', + 'Indian/Kerguelen', + 'Indian/Mahe', + 'Indian/Maldives', + 'Indian/Mauritius', + 'Indian/Mayotte', + 'Indian/Reunion', + 'Iran', + 'Israel', + 'Jamaica', + 'Japan', + 'Kwajalein', + 'Libya', + 'MET', + 'MST', + 'MST7MDT', + 'Mexico/BajaNorte', + 'Mexico/BajaSur', + 'Mexico/General', + 'NZ', + 'NZ-CHAT', + 'Navajo', + 'PRC', + 'PST8PDT', + 'Pacific/Apia', + 'Pacific/Auckland', + 'Pacific/Bougainville', + 'Pacific/Chatham', + 'Pacific/Chuuk', + 'Pacific/Easter', + 'Pacific/Efate', + 'Pacific/Enderbury', + 'Pacific/Fakaofo', + 'Pacific/Fiji', + 'Pacific/Funafuti', + 'Pacific/Galapagos', + 'Pacific/Gambier', + 'Pacific/Guadalcanal', + 'Pacific/Guam', + 'Pacific/Honolulu', + 'Pacific/Johnston', + 'Pacific/Kiritimati', + 'Pacific/Kosrae', + 'Pacific/Kwajalein', + 'Pacific/Majuro', + 'Pacific/Marquesas', + 'Pacific/Midway', + 'Pacific/Nauru', + 'Pacific/Niue', + 'Pacific/Norfolk', + 'Pacific/Noumea', + 'Pacific/Pago_Pago', + 'Pacific/Palau', + 'Pacific/Pitcairn', + 'Pacific/Pohnpei', + 'Pacific/Ponape', + 'Pacific/Port_Moresby', + 'Pacific/Rarotonga', + 'Pacific/Saipan', + 'Pacific/Samoa', + 'Pacific/Tahiti', + 'Pacific/Tarawa', + 'Pacific/Tongatapu', + 'Pacific/Truk', + 'Pacific/Wake', + 'Pacific/Wallis', + 'Pacific/Yap', + 'Poland', + 'Portugal', + 'ROC', + 'ROK', + 'Singapore', + 'Turkey', + 'UCT', + 'US/Alaska', + 'US/Aleutian', + 'US/Arizona', + 'US/Central', + 'US/East-Indiana', + 'US/Eastern', + 'US/Hawaii', + 'US/Indiana-Starke', + 'US/Michigan', + 'US/Mountain', + 'US/Pacific', + 'US/Samoa', + 'UTC', + 'Universal', + 'W-SU', + 'WET', + 'Zulu'] +all_timezones = LazyList( + tz for tz in all_timezones if resource_exists(tz)) + +all_timezones_set = LazySet(all_timezones) +common_timezones = \ +['Africa/Abidjan', + 'Africa/Accra', + 'Africa/Addis_Ababa', + 'Africa/Algiers', + 'Africa/Asmara', + 'Africa/Bamako', + 'Africa/Bangui', + 'Africa/Banjul', + 'Africa/Bissau', + 'Africa/Blantyre', + 'Africa/Brazzaville', + 'Africa/Bujumbura', + 'Africa/Cairo', + 'Africa/Casablanca', + 'Africa/Ceuta', + 'Africa/Conakry', + 'Africa/Dakar', + 'Africa/Dar_es_Salaam', + 'Africa/Djibouti', + 'Africa/Douala', + 'Africa/El_Aaiun', + 'Africa/Freetown', + 'Africa/Gaborone', + 'Africa/Harare', + 'Africa/Johannesburg', + 'Africa/Juba', + 'Africa/Kampala', + 'Africa/Khartoum', + 'Africa/Kigali', + 'Africa/Kinshasa', + 'Africa/Lagos', + 'Africa/Libreville', + 'Africa/Lome', + 'Africa/Luanda', + 'Africa/Lubumbashi', + 'Africa/Lusaka', + 'Africa/Malabo', + 'Africa/Maputo', + 'Africa/Maseru', + 'Africa/Mbabane', + 'Africa/Mogadishu', + 'Africa/Monrovia', + 'Africa/Nairobi', + 'Africa/Ndjamena', + 'Africa/Niamey', + 'Africa/Nouakchott', + 'Africa/Ouagadougou', + 'Africa/Porto-Novo', + 'Africa/Sao_Tome', + 'Africa/Tripoli', + 'Africa/Tunis', + 'Africa/Windhoek', + 'America/Adak', + 'America/Anchorage', + 'America/Anguilla', + 'America/Antigua', + 'America/Araguaina', + 'America/Argentina/Buenos_Aires', + 'America/Argentina/Catamarca', + 'America/Argentina/Cordoba', + 'America/Argentina/Jujuy', + 'America/Argentina/La_Rioja', + 'America/Argentina/Mendoza', + 'America/Argentina/Rio_Gallegos', + 'America/Argentina/Salta', + 'America/Argentina/San_Juan', + 'America/Argentina/San_Luis', + 'America/Argentina/Tucuman', + 'America/Argentina/Ushuaia', + 'America/Aruba', + 'America/Asuncion', + 'America/Atikokan', + 'America/Bahia', + 'America/Bahia_Banderas', + 'America/Barbados', + 'America/Belem', + 'America/Belize', + 'America/Blanc-Sablon', + 'America/Boa_Vista', + 'America/Bogota', + 'America/Boise', + 'America/Cambridge_Bay', + 'America/Campo_Grande', + 'America/Cancun', + 'America/Caracas', + 'America/Cayenne', + 'America/Cayman', + 'America/Chicago', + 'America/Chihuahua', + 'America/Costa_Rica', + 'America/Creston', + 'America/Cuiaba', + 'America/Curacao', + 'America/Danmarkshavn', + 'America/Dawson', + 'America/Dawson_Creek', + 'America/Denver', + 'America/Detroit', + 'America/Dominica', + 'America/Edmonton', + 'America/Eirunepe', + 'America/El_Salvador', + 'America/Fort_Nelson', + 'America/Fortaleza', + 'America/Glace_Bay', + 'America/Godthab', + 'America/Goose_Bay', + 'America/Grand_Turk', + 'America/Grenada', + 'America/Guadeloupe', + 'America/Guatemala', + 'America/Guayaquil', + 'America/Guyana', + 'America/Halifax', + 'America/Havana', + 'America/Hermosillo', + 'America/Indiana/Indianapolis', + 'America/Indiana/Knox', + 'America/Indiana/Marengo', + 'America/Indiana/Petersburg', + 'America/Indiana/Tell_City', + 'America/Indiana/Vevay', + 'America/Indiana/Vincennes', + 'America/Indiana/Winamac', + 'America/Inuvik', + 'America/Iqaluit', + 'America/Jamaica', + 'America/Juneau', + 'America/Kentucky/Louisville', + 'America/Kentucky/Monticello', + 'America/Kralendijk', + 'America/La_Paz', + 'America/Lima', + 'America/Los_Angeles', + 'America/Lower_Princes', + 'America/Maceio', + 'America/Managua', + 'America/Manaus', + 'America/Marigot', + 'America/Martinique', + 'America/Matamoros', + 'America/Mazatlan', + 'America/Menominee', + 'America/Merida', + 'America/Metlakatla', + 'America/Mexico_City', + 'America/Miquelon', + 'America/Moncton', + 'America/Monterrey', + 'America/Montevideo', + 'America/Montserrat', + 'America/Nassau', + 'America/New_York', + 'America/Nipigon', + 'America/Nome', + 'America/Noronha', + 'America/North_Dakota/Beulah', + 'America/North_Dakota/Center', + 'America/North_Dakota/New_Salem', + 'America/Ojinaga', + 'America/Panama', + 'America/Pangnirtung', + 'America/Paramaribo', + 'America/Phoenix', + 'America/Port-au-Prince', + 'America/Port_of_Spain', + 'America/Porto_Velho', + 'America/Puerto_Rico', + 'America/Punta_Arenas', + 'America/Rainy_River', + 'America/Rankin_Inlet', + 'America/Recife', + 'America/Regina', + 'America/Resolute', + 'America/Rio_Branco', + 'America/Santarem', + 'America/Santiago', + 'America/Santo_Domingo', + 'America/Sao_Paulo', + 'America/Scoresbysund', + 'America/Sitka', + 'America/St_Barthelemy', + 'America/St_Johns', + 'America/St_Kitts', + 'America/St_Lucia', + 'America/St_Thomas', + 'America/St_Vincent', + 'America/Swift_Current', + 'America/Tegucigalpa', + 'America/Thule', + 'America/Thunder_Bay', + 'America/Tijuana', + 'America/Toronto', + 'America/Tortola', + 'America/Vancouver', + 'America/Whitehorse', + 'America/Winnipeg', + 'America/Yakutat', + 'America/Yellowknife', + 'Antarctica/Casey', + 'Antarctica/Davis', + 'Antarctica/DumontDUrville', + 'Antarctica/Macquarie', + 'Antarctica/Mawson', + 'Antarctica/McMurdo', + 'Antarctica/Palmer', + 'Antarctica/Rothera', + 'Antarctica/Syowa', + 'Antarctica/Troll', + 'Antarctica/Vostok', + 'Arctic/Longyearbyen', + 'Asia/Aden', + 'Asia/Almaty', + 'Asia/Amman', + 'Asia/Anadyr', + 'Asia/Aqtau', + 'Asia/Aqtobe', + 'Asia/Ashgabat', + 'Asia/Atyrau', + 'Asia/Baghdad', + 'Asia/Bahrain', + 'Asia/Baku', + 'Asia/Bangkok', + 'Asia/Barnaul', + 'Asia/Beirut', + 'Asia/Bishkek', + 'Asia/Brunei', + 'Asia/Chita', + 'Asia/Choibalsan', + 'Asia/Colombo', + 'Asia/Damascus', + 'Asia/Dhaka', + 'Asia/Dili', + 'Asia/Dubai', + 'Asia/Dushanbe', + 'Asia/Famagusta', + 'Asia/Gaza', + 'Asia/Hebron', + 'Asia/Ho_Chi_Minh', + 'Asia/Hong_Kong', + 'Asia/Hovd', + 'Asia/Irkutsk', + 'Asia/Jakarta', + 'Asia/Jayapura', + 'Asia/Jerusalem', + 'Asia/Kabul', + 'Asia/Kamchatka', + 'Asia/Karachi', + 'Asia/Kathmandu', + 'Asia/Khandyga', + 'Asia/Kolkata', + 'Asia/Krasnoyarsk', + 'Asia/Kuala_Lumpur', + 'Asia/Kuching', + 'Asia/Kuwait', + 'Asia/Macau', + 'Asia/Magadan', + 'Asia/Makassar', + 'Asia/Manila', + 'Asia/Muscat', + 'Asia/Nicosia', + 'Asia/Novokuznetsk', + 'Asia/Novosibirsk', + 'Asia/Omsk', + 'Asia/Oral', + 'Asia/Phnom_Penh', + 'Asia/Pontianak', + 'Asia/Pyongyang', + 'Asia/Qatar', + 'Asia/Qyzylorda', + 'Asia/Riyadh', + 'Asia/Sakhalin', + 'Asia/Samarkand', + 'Asia/Seoul', + 'Asia/Shanghai', + 'Asia/Singapore', + 'Asia/Srednekolymsk', + 'Asia/Taipei', + 'Asia/Tashkent', + 'Asia/Tbilisi', + 'Asia/Tehran', + 'Asia/Thimphu', + 'Asia/Tokyo', + 'Asia/Tomsk', + 'Asia/Ulaanbaatar', + 'Asia/Urumqi', + 'Asia/Ust-Nera', + 'Asia/Vientiane', + 'Asia/Vladivostok', + 'Asia/Yakutsk', + 'Asia/Yangon', + 'Asia/Yekaterinburg', + 'Asia/Yerevan', + 'Atlantic/Azores', + 'Atlantic/Bermuda', + 'Atlantic/Canary', + 'Atlantic/Cape_Verde', + 'Atlantic/Faroe', + 'Atlantic/Madeira', + 'Atlantic/Reykjavik', + 'Atlantic/South_Georgia', + 'Atlantic/St_Helena', + 'Atlantic/Stanley', + 'Australia/Adelaide', + 'Australia/Brisbane', + 'Australia/Broken_Hill', + 'Australia/Currie', + 'Australia/Darwin', + 'Australia/Eucla', + 'Australia/Hobart', + 'Australia/Lindeman', + 'Australia/Lord_Howe', + 'Australia/Melbourne', + 'Australia/Perth', + 'Australia/Sydney', + 'Canada/Atlantic', + 'Canada/Central', + 'Canada/Eastern', + 'Canada/Mountain', + 'Canada/Newfoundland', + 'Canada/Pacific', + 'Europe/Amsterdam', + 'Europe/Andorra', + 'Europe/Astrakhan', + 'Europe/Athens', + 'Europe/Belgrade', + 'Europe/Berlin', + 'Europe/Bratislava', + 'Europe/Brussels', + 'Europe/Bucharest', + 'Europe/Budapest', + 'Europe/Busingen', + 'Europe/Chisinau', + 'Europe/Copenhagen', + 'Europe/Dublin', + 'Europe/Gibraltar', + 'Europe/Guernsey', + 'Europe/Helsinki', + 'Europe/Isle_of_Man', + 'Europe/Istanbul', + 'Europe/Jersey', + 'Europe/Kaliningrad', + 'Europe/Kiev', + 'Europe/Kirov', + 'Europe/Lisbon', + 'Europe/Ljubljana', + 'Europe/London', + 'Europe/Luxembourg', + 'Europe/Madrid', + 'Europe/Malta', + 'Europe/Mariehamn', + 'Europe/Minsk', + 'Europe/Monaco', + 'Europe/Moscow', + 'Europe/Oslo', + 'Europe/Paris', + 'Europe/Podgorica', + 'Europe/Prague', + 'Europe/Riga', + 'Europe/Rome', + 'Europe/Samara', + 'Europe/San_Marino', + 'Europe/Sarajevo', + 'Europe/Saratov', + 'Europe/Simferopol', + 'Europe/Skopje', + 'Europe/Sofia', + 'Europe/Stockholm', + 'Europe/Tallinn', + 'Europe/Tirane', + 'Europe/Ulyanovsk', + 'Europe/Uzhgorod', + 'Europe/Vaduz', + 'Europe/Vatican', + 'Europe/Vienna', + 'Europe/Vilnius', + 'Europe/Volgograd', + 'Europe/Warsaw', + 'Europe/Zagreb', + 'Europe/Zaporozhye', + 'Europe/Zurich', + 'GMT', + 'Indian/Antananarivo', + 'Indian/Chagos', + 'Indian/Christmas', + 'Indian/Cocos', + 'Indian/Comoro', + 'Indian/Kerguelen', + 'Indian/Mahe', + 'Indian/Maldives', + 'Indian/Mauritius', + 'Indian/Mayotte', + 'Indian/Reunion', + 'Pacific/Apia', + 'Pacific/Auckland', + 'Pacific/Bougainville', + 'Pacific/Chatham', + 'Pacific/Chuuk', + 'Pacific/Easter', + 'Pacific/Efate', + 'Pacific/Enderbury', + 'Pacific/Fakaofo', + 'Pacific/Fiji', + 'Pacific/Funafuti', + 'Pacific/Galapagos', + 'Pacific/Gambier', + 'Pacific/Guadalcanal', + 'Pacific/Guam', + 'Pacific/Honolulu', + 'Pacific/Kiritimati', + 'Pacific/Kosrae', + 'Pacific/Kwajalein', + 'Pacific/Majuro', + 'Pacific/Marquesas', + 'Pacific/Midway', + 'Pacific/Nauru', + 'Pacific/Niue', + 'Pacific/Norfolk', + 'Pacific/Noumea', + 'Pacific/Pago_Pago', + 'Pacific/Palau', + 'Pacific/Pitcairn', + 'Pacific/Pohnpei', + 'Pacific/Port_Moresby', + 'Pacific/Rarotonga', + 'Pacific/Saipan', + 'Pacific/Tahiti', + 'Pacific/Tarawa', + 'Pacific/Tongatapu', + 'Pacific/Wake', + 'Pacific/Wallis', + 'US/Alaska', + 'US/Arizona', + 'US/Central', + 'US/Eastern', + 'US/Hawaii', + 'US/Mountain', + 'US/Pacific', + 'UTC'] +common_timezones = LazyList( + tz for tz in common_timezones if tz in all_timezones) + +common_timezones_set = LazySet(common_timezones) diff --git a/libs/pytz/exceptions.py b/libs/pytz/exceptions.py new file mode 100644 index 00000000..18df33e8 --- /dev/null +++ b/libs/pytz/exceptions.py @@ -0,0 +1,48 @@ +''' +Custom exceptions raised by pytz. +''' + +__all__ = [ + 'UnknownTimeZoneError', 'InvalidTimeError', 'AmbiguousTimeError', + 'NonExistentTimeError', +] + + +class UnknownTimeZoneError(KeyError): + '''Exception raised when pytz is passed an unknown timezone. + + >>> isinstance(UnknownTimeZoneError(), LookupError) + True + + This class is actually a subclass of KeyError to provide backwards + compatibility with code relying on the undocumented behavior of earlier + pytz releases. + + >>> isinstance(UnknownTimeZoneError(), KeyError) + True + ''' + pass + + +class InvalidTimeError(Exception): + '''Base class for invalid time exceptions.''' + + +class AmbiguousTimeError(InvalidTimeError): + '''Exception raised when attempting to create an ambiguous wallclock time. + + At the end of a DST transition period, a particular wallclock time will + occur twice (once before the clocks are set back, once after). Both + possibilities may be correct, unless further information is supplied. + + See DstTzInfo.normalize() for more info + ''' + + +class NonExistentTimeError(InvalidTimeError): + '''Exception raised when attempting to create a wallclock time that + cannot exist. + + At the start of a DST transition period, the wallclock time jumps forward. + The instants jumped over never occur. + ''' diff --git a/libs/pytz/lazy.py b/libs/pytz/lazy.py new file mode 100644 index 00000000..39344fc1 --- /dev/null +++ b/libs/pytz/lazy.py @@ -0,0 +1,172 @@ +from threading import RLock +try: + from collections.abc import Mapping as DictMixin +except ImportError: # Python < 3.3 + try: + from UserDict import DictMixin # Python 2 + except ImportError: # Python 3.0-3.3 + from collections import Mapping as DictMixin + + +# With lazy loading, we might end up with multiple threads triggering +# it at the same time. We need a lock. +_fill_lock = RLock() + + +class LazyDict(DictMixin): + """Dictionary populated on first use.""" + data = None + + def __getitem__(self, key): + if self.data is None: + _fill_lock.acquire() + try: + if self.data is None: + self._fill() + finally: + _fill_lock.release() + return self.data[key.upper()] + + def __contains__(self, key): + if self.data is None: + _fill_lock.acquire() + try: + if self.data is None: + self._fill() + finally: + _fill_lock.release() + return key in self.data + + def __iter__(self): + if self.data is None: + _fill_lock.acquire() + try: + if self.data is None: + self._fill() + finally: + _fill_lock.release() + return iter(self.data) + + def __len__(self): + if self.data is None: + _fill_lock.acquire() + try: + if self.data is None: + self._fill() + finally: + _fill_lock.release() + return len(self.data) + + def keys(self): + if self.data is None: + _fill_lock.acquire() + try: + if self.data is None: + self._fill() + finally: + _fill_lock.release() + return self.data.keys() + + +class LazyList(list): + """List populated on first use.""" + + _props = [ + '__str__', '__repr__', '__unicode__', + '__hash__', '__sizeof__', '__cmp__', + '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', + 'append', 'count', 'index', 'extend', 'insert', 'pop', 'remove', + 'reverse', 'sort', '__add__', '__radd__', '__iadd__', '__mul__', + '__rmul__', '__imul__', '__contains__', '__len__', '__nonzero__', + '__getitem__', '__setitem__', '__delitem__', '__iter__', + '__reversed__', '__getslice__', '__setslice__', '__delslice__'] + + def __new__(cls, fill_iter=None): + + if fill_iter is None: + return list() + + # We need a new class as we will be dynamically messing with its + # methods. + class LazyList(list): + pass + + fill_iter = [fill_iter] + + def lazy(name): + def _lazy(self, *args, **kw): + _fill_lock.acquire() + try: + if len(fill_iter) > 0: + list.extend(self, fill_iter.pop()) + for method_name in cls._props: + delattr(LazyList, method_name) + finally: + _fill_lock.release() + return getattr(list, name)(self, *args, **kw) + return _lazy + + for name in cls._props: + setattr(LazyList, name, lazy(name)) + + new_list = LazyList() + return new_list + +# Not all versions of Python declare the same magic methods. +# Filter out properties that don't exist in this version of Python +# from the list. +LazyList._props = [prop for prop in LazyList._props if hasattr(list, prop)] + + +class LazySet(set): + """Set populated on first use.""" + + _props = ( + '__str__', '__repr__', '__unicode__', + '__hash__', '__sizeof__', '__cmp__', + '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', + '__contains__', '__len__', '__nonzero__', + '__getitem__', '__setitem__', '__delitem__', '__iter__', + '__sub__', '__and__', '__xor__', '__or__', + '__rsub__', '__rand__', '__rxor__', '__ror__', + '__isub__', '__iand__', '__ixor__', '__ior__', + 'add', 'clear', 'copy', 'difference', 'difference_update', + 'discard', 'intersection', 'intersection_update', 'isdisjoint', + 'issubset', 'issuperset', 'pop', 'remove', + 'symmetric_difference', 'symmetric_difference_update', + 'union', 'update') + + def __new__(cls, fill_iter=None): + + if fill_iter is None: + return set() + + class LazySet(set): + pass + + fill_iter = [fill_iter] + + def lazy(name): + def _lazy(self, *args, **kw): + _fill_lock.acquire() + try: + if len(fill_iter) > 0: + for i in fill_iter.pop(): + set.add(self, i) + for method_name in cls._props: + delattr(LazySet, method_name) + finally: + _fill_lock.release() + return getattr(set, name)(self, *args, **kw) + return _lazy + + for name in cls._props: + setattr(LazySet, name, lazy(name)) + + new_set = LazySet() + return new_set + +# Not all versions of Python declare the same magic methods. +# Filter out properties that don't exist in this version of Python +# from the list. +LazySet._props = [prop for prop in LazySet._props if hasattr(set, prop)] diff --git a/libs/pytz/reference.py b/libs/pytz/reference.py new file mode 100644 index 00000000..f765ca0a --- /dev/null +++ b/libs/pytz/reference.py @@ -0,0 +1,140 @@ +''' +Reference tzinfo implementations from the Python docs. +Used for testing against as they are only correct for the years +1987 to 2006. Do not use these for real code. +''' + +from datetime import tzinfo, timedelta, datetime +from pytz import HOUR, ZERO, UTC + +__all__ = [ + 'FixedOffset', + 'LocalTimezone', + 'USTimeZone', + 'Eastern', + 'Central', + 'Mountain', + 'Pacific', + 'UTC' +] + + +# A class building tzinfo objects for fixed-offset time zones. +# Note that FixedOffset(0, "UTC") is a different way to build a +# UTC tzinfo object. +class FixedOffset(tzinfo): + """Fixed offset in minutes east from UTC.""" + + def __init__(self, offset, name): + self.__offset = timedelta(minutes=offset) + self.__name = name + + def utcoffset(self, dt): + return self.__offset + + def tzname(self, dt): + return self.__name + + def dst(self, dt): + return ZERO + + +import time as _time + +STDOFFSET = timedelta(seconds=-_time.timezone) +if _time.daylight: + DSTOFFSET = timedelta(seconds=-_time.altzone) +else: + DSTOFFSET = STDOFFSET + +DSTDIFF = DSTOFFSET - STDOFFSET + + +# A class capturing the platform's idea of local time. +class LocalTimezone(tzinfo): + + def utcoffset(self, dt): + if self._isdst(dt): + return DSTOFFSET + else: + return STDOFFSET + + def dst(self, dt): + if self._isdst(dt): + return DSTDIFF + else: + return ZERO + + def tzname(self, dt): + return _time.tzname[self._isdst(dt)] + + def _isdst(self, dt): + tt = (dt.year, dt.month, dt.day, + dt.hour, dt.minute, dt.second, + dt.weekday(), 0, -1) + stamp = _time.mktime(tt) + tt = _time.localtime(stamp) + return tt.tm_isdst > 0 + +Local = LocalTimezone() + + +def first_sunday_on_or_after(dt): + days_to_go = 6 - dt.weekday() + if days_to_go: + dt += timedelta(days_to_go) + return dt + + +# In the US, DST starts at 2am (standard time) on the first Sunday in April. +DSTSTART = datetime(1, 4, 1, 2) +# and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct. +# which is the first Sunday on or after Oct 25. +DSTEND = datetime(1, 10, 25, 1) + + +# A complete implementation of current DST rules for major US time zones. +class USTimeZone(tzinfo): + + def __init__(self, hours, reprname, stdname, dstname): + self.stdoffset = timedelta(hours=hours) + self.reprname = reprname + self.stdname = stdname + self.dstname = dstname + + def __repr__(self): + return self.reprname + + def tzname(self, dt): + if self.dst(dt): + return self.dstname + else: + return self.stdname + + def utcoffset(self, dt): + return self.stdoffset + self.dst(dt) + + def dst(self, dt): + if dt is None or dt.tzinfo is None: + # An exception may be sensible here, in one or both cases. + # It depends on how you want to treat them. The default + # fromutc() implementation (called by the default astimezone() + # implementation) passes a datetime with dt.tzinfo is self. + return ZERO + assert dt.tzinfo is self + + # Find first Sunday in April & the last in October. + start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year)) + end = first_sunday_on_or_after(DSTEND.replace(year=dt.year)) + + # Can't compare naive to aware objects, so strip the timezone from + # dt first. + if start <= dt.replace(tzinfo=None) < end: + return HOUR + else: + return ZERO + +Eastern = USTimeZone(-5, "Eastern", "EST", "EDT") +Central = USTimeZone(-6, "Central", "CST", "CDT") +Mountain = USTimeZone(-7, "Mountain", "MST", "MDT") +Pacific = USTimeZone(-8, "Pacific", "PST", "PDT") diff --git a/libs/pytz/tzfile.py b/libs/pytz/tzfile.py new file mode 100644 index 00000000..25117f32 --- /dev/null +++ b/libs/pytz/tzfile.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python +''' +$Id: tzfile.py,v 1.8 2004/06/03 00:15:24 zenzen Exp $ +''' + +from datetime import datetime +from struct import unpack, calcsize + +from pytz.tzinfo import StaticTzInfo, DstTzInfo, memorized_ttinfo +from pytz.tzinfo import memorized_datetime, memorized_timedelta + + +def _byte_string(s): + """Cast a string or byte string to an ASCII byte string.""" + return s.encode('ASCII') + +_NULL = _byte_string('\0') + + +def _std_string(s): + """Cast a string or byte string to an ASCII string.""" + return str(s.decode('ASCII')) + + +def build_tzinfo(zone, fp): + head_fmt = '>4s c 15x 6l' + head_size = calcsize(head_fmt) + (magic, format, ttisgmtcnt, ttisstdcnt, leapcnt, timecnt, + typecnt, charcnt) = unpack(head_fmt, fp.read(head_size)) + + # Make sure it is a tzfile(5) file + assert magic == _byte_string('TZif'), 'Got magic %s' % repr(magic) + + # Read out the transition times, localtime indices and ttinfo structures. + data_fmt = '>%(timecnt)dl %(timecnt)dB %(ttinfo)s %(charcnt)ds' % dict( + timecnt=timecnt, ttinfo='lBB' * typecnt, charcnt=charcnt) + data_size = calcsize(data_fmt) + data = unpack(data_fmt, fp.read(data_size)) + + # make sure we unpacked the right number of values + assert len(data) == 2 * timecnt + 3 * typecnt + 1 + transitions = [memorized_datetime(trans) + for trans in data[:timecnt]] + lindexes = list(data[timecnt:2 * timecnt]) + ttinfo_raw = data[2 * timecnt:-1] + tznames_raw = data[-1] + del data + + # Process ttinfo into separate structs + ttinfo = [] + tznames = {} + i = 0 + while i < len(ttinfo_raw): + # have we looked up this timezone name yet? + tzname_offset = ttinfo_raw[i + 2] + if tzname_offset not in tznames: + nul = tznames_raw.find(_NULL, tzname_offset) + if nul < 0: + nul = len(tznames_raw) + tznames[tzname_offset] = _std_string( + tznames_raw[tzname_offset:nul]) + ttinfo.append((ttinfo_raw[i], + bool(ttinfo_raw[i + 1]), + tznames[tzname_offset])) + i += 3 + + # Now build the timezone object + if len(ttinfo) == 1 or len(transitions) == 0: + ttinfo[0][0], ttinfo[0][2] + cls = type(zone, (StaticTzInfo,), dict( + zone=zone, + _utcoffset=memorized_timedelta(ttinfo[0][0]), + _tzname=ttinfo[0][2])) + else: + # Early dates use the first standard time ttinfo + i = 0 + while ttinfo[i][1]: + i += 1 + if ttinfo[i] == ttinfo[lindexes[0]]: + transitions[0] = datetime.min + else: + transitions.insert(0, datetime.min) + lindexes.insert(0, i) + + # calculate transition info + transition_info = [] + for i in range(len(transitions)): + inf = ttinfo[lindexes[i]] + utcoffset = inf[0] + if not inf[1]: + dst = 0 + else: + for j in range(i - 1, -1, -1): + prev_inf = ttinfo[lindexes[j]] + if not prev_inf[1]: + break + dst = inf[0] - prev_inf[0] # dst offset + + # Bad dst? Look further. DST > 24 hours happens when + # a timzone has moved across the international dateline. + if dst <= 0 or dst > 3600 * 3: + for j in range(i + 1, len(transitions)): + stdinf = ttinfo[lindexes[j]] + if not stdinf[1]: + dst = inf[0] - stdinf[0] + if dst > 0: + break # Found a useful std time. + + tzname = inf[2] + + # Round utcoffset and dst to the nearest minute or the + # datetime library will complain. Conversions to these timezones + # might be up to plus or minus 30 seconds out, but it is + # the best we can do. + utcoffset = int((utcoffset + 30) // 60) * 60 + dst = int((dst + 30) // 60) * 60 + transition_info.append(memorized_ttinfo(utcoffset, dst, tzname)) + + cls = type(zone, (DstTzInfo,), dict( + zone=zone, + _utc_transition_times=transitions, + _transition_info=transition_info)) + + return cls() + +if __name__ == '__main__': + import os.path + from pprint import pprint + base = os.path.join(os.path.dirname(__file__), 'zoneinfo') + tz = build_tzinfo('Australia/Melbourne', + open(os.path.join(base, 'Australia', 'Melbourne'), 'rb')) + tz = build_tzinfo('US/Eastern', + open(os.path.join(base, 'US', 'Eastern'), 'rb')) + pprint(tz._utc_transition_times) diff --git a/libs/pytz/tzinfo.py b/libs/pytz/tzinfo.py new file mode 100644 index 00000000..725978d5 --- /dev/null +++ b/libs/pytz/tzinfo.py @@ -0,0 +1,577 @@ +'''Base classes and helpers for building zone specific tzinfo classes''' + +from datetime import datetime, timedelta, tzinfo +from bisect import bisect_right +try: + set +except NameError: + from sets import Set as set + +import pytz +from pytz.exceptions import AmbiguousTimeError, NonExistentTimeError + +__all__ = [] + +_timedelta_cache = {} + + +def memorized_timedelta(seconds): + '''Create only one instance of each distinct timedelta''' + try: + return _timedelta_cache[seconds] + except KeyError: + delta = timedelta(seconds=seconds) + _timedelta_cache[seconds] = delta + return delta + +_epoch = datetime.utcfromtimestamp(0) +_datetime_cache = {0: _epoch} + + +def memorized_datetime(seconds): + '''Create only one instance of each distinct datetime''' + try: + return _datetime_cache[seconds] + except KeyError: + # NB. We can't just do datetime.utcfromtimestamp(seconds) as this + # fails with negative values under Windows (Bug #90096) + dt = _epoch + timedelta(seconds=seconds) + _datetime_cache[seconds] = dt + return dt + +_ttinfo_cache = {} + + +def memorized_ttinfo(*args): + '''Create only one instance of each distinct tuple''' + try: + return _ttinfo_cache[args] + except KeyError: + ttinfo = ( + memorized_timedelta(args[0]), + memorized_timedelta(args[1]), + args[2] + ) + _ttinfo_cache[args] = ttinfo + return ttinfo + +_notime = memorized_timedelta(0) + + +def _to_seconds(td): + '''Convert a timedelta to seconds''' + return td.seconds + td.days * 24 * 60 * 60 + + +class BaseTzInfo(tzinfo): + # Overridden in subclass + _utcoffset = None + _tzname = None + zone = None + + def __str__(self): + return self.zone + + +class StaticTzInfo(BaseTzInfo): + '''A timezone that has a constant offset from UTC + + These timezones are rare, as most locations have changed their + offset at some point in their history + ''' + def fromutc(self, dt): + '''See datetime.tzinfo.fromutc''' + if dt.tzinfo is not None and dt.tzinfo is not self: + raise ValueError('fromutc: dt.tzinfo is not self') + return (dt + self._utcoffset).replace(tzinfo=self) + + def utcoffset(self, dt, is_dst=None): + '''See datetime.tzinfo.utcoffset + + is_dst is ignored for StaticTzInfo, and exists only to + retain compatibility with DstTzInfo. + ''' + return self._utcoffset + + def dst(self, dt, is_dst=None): + '''See datetime.tzinfo.dst + + is_dst is ignored for StaticTzInfo, and exists only to + retain compatibility with DstTzInfo. + ''' + return _notime + + def tzname(self, dt, is_dst=None): + '''See datetime.tzinfo.tzname + + is_dst is ignored for StaticTzInfo, and exists only to + retain compatibility with DstTzInfo. + ''' + return self._tzname + + def localize(self, dt, is_dst=False): + '''Convert naive time to local time''' + if dt.tzinfo is not None: + raise ValueError('Not naive datetime (tzinfo is already set)') + return dt.replace(tzinfo=self) + + def normalize(self, dt, is_dst=False): + '''Correct the timezone information on the given datetime. + + This is normally a no-op, as StaticTzInfo timezones never have + ambiguous cases to correct: + + >>> from pytz import timezone + >>> gmt = timezone('GMT') + >>> isinstance(gmt, StaticTzInfo) + True + >>> dt = datetime(2011, 5, 8, 1, 2, 3, tzinfo=gmt) + >>> gmt.normalize(dt) is dt + True + + The supported method of converting between timezones is to use + datetime.astimezone(). Currently normalize() also works: + + >>> la = timezone('America/Los_Angeles') + >>> dt = la.localize(datetime(2011, 5, 7, 1, 2, 3)) + >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)' + >>> gmt.normalize(dt).strftime(fmt) + '2011-05-07 08:02:03 GMT (+0000)' + ''' + if dt.tzinfo is self: + return dt + if dt.tzinfo is None: + raise ValueError('Naive time - no tzinfo set') + return dt.astimezone(self) + + def __repr__(self): + return '' % (self.zone,) + + def __reduce__(self): + # Special pickle to zone remains a singleton and to cope with + # database changes. + return pytz._p, (self.zone,) + + +class DstTzInfo(BaseTzInfo): + '''A timezone that has a variable offset from UTC + + The offset might change if daylight saving time comes into effect, + or at a point in history when the region decides to change their + timezone definition. + ''' + # Overridden in subclass + + # Sorted list of DST transition times, UTC + _utc_transition_times = None + + # [(utcoffset, dstoffset, tzname)] corresponding to + # _utc_transition_times entries + _transition_info = None + + zone = None + + # Set in __init__ + + _tzinfos = None + _dst = None # DST offset + + def __init__(self, _inf=None, _tzinfos=None): + if _inf: + self._tzinfos = _tzinfos + self._utcoffset, self._dst, self._tzname = _inf + else: + _tzinfos = {} + self._tzinfos = _tzinfos + self._utcoffset, self._dst, self._tzname = ( + self._transition_info[0]) + _tzinfos[self._transition_info[0]] = self + for inf in self._transition_info[1:]: + if inf not in _tzinfos: + _tzinfos[inf] = self.__class__(inf, _tzinfos) + + def fromutc(self, dt): + '''See datetime.tzinfo.fromutc''' + if (dt.tzinfo is not None and + getattr(dt.tzinfo, '_tzinfos', None) is not self._tzinfos): + raise ValueError('fromutc: dt.tzinfo is not self') + dt = dt.replace(tzinfo=None) + idx = max(0, bisect_right(self._utc_transition_times, dt) - 1) + inf = self._transition_info[idx] + return (dt + inf[0]).replace(tzinfo=self._tzinfos[inf]) + + def normalize(self, dt): + '''Correct the timezone information on the given datetime + + If date arithmetic crosses DST boundaries, the tzinfo + is not magically adjusted. This method normalizes the + tzinfo to the correct one. + + To test, first we need to do some setup + + >>> from pytz import timezone + >>> utc = timezone('UTC') + >>> eastern = timezone('US/Eastern') + >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)' + + We next create a datetime right on an end-of-DST transition point, + the instant when the wallclocks are wound back one hour. + + >>> utc_dt = datetime(2002, 10, 27, 6, 0, 0, tzinfo=utc) + >>> loc_dt = utc_dt.astimezone(eastern) + >>> loc_dt.strftime(fmt) + '2002-10-27 01:00:00 EST (-0500)' + + Now, if we subtract a few minutes from it, note that the timezone + information has not changed. + + >>> before = loc_dt - timedelta(minutes=10) + >>> before.strftime(fmt) + '2002-10-27 00:50:00 EST (-0500)' + + But we can fix that by calling the normalize method + + >>> before = eastern.normalize(before) + >>> before.strftime(fmt) + '2002-10-27 01:50:00 EDT (-0400)' + + The supported method of converting between timezones is to use + datetime.astimezone(). Currently, normalize() also works: + + >>> th = timezone('Asia/Bangkok') + >>> am = timezone('Europe/Amsterdam') + >>> dt = th.localize(datetime(2011, 5, 7, 1, 2, 3)) + >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)' + >>> am.normalize(dt).strftime(fmt) + '2011-05-06 20:02:03 CEST (+0200)' + ''' + if dt.tzinfo is None: + raise ValueError('Naive time - no tzinfo set') + + # Convert dt in localtime to UTC + offset = dt.tzinfo._utcoffset + dt = dt.replace(tzinfo=None) + dt = dt - offset + # convert it back, and return it + return self.fromutc(dt) + + def localize(self, dt, is_dst=False): + '''Convert naive time to local time. + + This method should be used to construct localtimes, rather + than passing a tzinfo argument to a datetime constructor. + + is_dst is used to determine the correct timezone in the ambigous + period at the end of daylight saving time. + + >>> from pytz import timezone + >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)' + >>> amdam = timezone('Europe/Amsterdam') + >>> dt = datetime(2004, 10, 31, 2, 0, 0) + >>> loc_dt1 = amdam.localize(dt, is_dst=True) + >>> loc_dt2 = amdam.localize(dt, is_dst=False) + >>> loc_dt1.strftime(fmt) + '2004-10-31 02:00:00 CEST (+0200)' + >>> loc_dt2.strftime(fmt) + '2004-10-31 02:00:00 CET (+0100)' + >>> str(loc_dt2 - loc_dt1) + '1:00:00' + + Use is_dst=None to raise an AmbiguousTimeError for ambiguous + times at the end of daylight saving time + + >>> try: + ... loc_dt1 = amdam.localize(dt, is_dst=None) + ... except AmbiguousTimeError: + ... print('Ambiguous') + Ambiguous + + is_dst defaults to False + + >>> amdam.localize(dt) == amdam.localize(dt, False) + True + + is_dst is also used to determine the correct timezone in the + wallclock times jumped over at the start of daylight saving time. + + >>> pacific = timezone('US/Pacific') + >>> dt = datetime(2008, 3, 9, 2, 0, 0) + >>> ploc_dt1 = pacific.localize(dt, is_dst=True) + >>> ploc_dt2 = pacific.localize(dt, is_dst=False) + >>> ploc_dt1.strftime(fmt) + '2008-03-09 02:00:00 PDT (-0700)' + >>> ploc_dt2.strftime(fmt) + '2008-03-09 02:00:00 PST (-0800)' + >>> str(ploc_dt2 - ploc_dt1) + '1:00:00' + + Use is_dst=None to raise a NonExistentTimeError for these skipped + times. + + >>> try: + ... loc_dt1 = pacific.localize(dt, is_dst=None) + ... except NonExistentTimeError: + ... print('Non-existent') + Non-existent + ''' + if dt.tzinfo is not None: + raise ValueError('Not naive datetime (tzinfo is already set)') + + # Find the two best possibilities. + possible_loc_dt = set() + for delta in [timedelta(days=-1), timedelta(days=1)]: + loc_dt = dt + delta + idx = max(0, bisect_right( + self._utc_transition_times, loc_dt) - 1) + inf = self._transition_info[idx] + tzinfo = self._tzinfos[inf] + loc_dt = tzinfo.normalize(dt.replace(tzinfo=tzinfo)) + if loc_dt.replace(tzinfo=None) == dt: + possible_loc_dt.add(loc_dt) + + if len(possible_loc_dt) == 1: + return possible_loc_dt.pop() + + # If there are no possibly correct timezones, we are attempting + # to convert a time that never happened - the time period jumped + # during the start-of-DST transition period. + if len(possible_loc_dt) == 0: + # If we refuse to guess, raise an exception. + if is_dst is None: + raise NonExistentTimeError(dt) + + # If we are forcing the pre-DST side of the DST transition, we + # obtain the correct timezone by winding the clock forward a few + # hours. + elif is_dst: + return self.localize( + dt + timedelta(hours=6), is_dst=True) - timedelta(hours=6) + + # If we are forcing the post-DST side of the DST transition, we + # obtain the correct timezone by winding the clock back. + else: + return self.localize( + dt - timedelta(hours=6), + is_dst=False) + timedelta(hours=6) + + # If we get this far, we have multiple possible timezones - this + # is an ambiguous case occuring during the end-of-DST transition. + + # If told to be strict, raise an exception since we have an + # ambiguous case + if is_dst is None: + raise AmbiguousTimeError(dt) + + # Filter out the possiblilities that don't match the requested + # is_dst + filtered_possible_loc_dt = [ + p for p in possible_loc_dt if bool(p.tzinfo._dst) == is_dst + ] + + # Hopefully we only have one possibility left. Return it. + if len(filtered_possible_loc_dt) == 1: + return filtered_possible_loc_dt[0] + + if len(filtered_possible_loc_dt) == 0: + filtered_possible_loc_dt = list(possible_loc_dt) + + # If we get this far, we have in a wierd timezone transition + # where the clocks have been wound back but is_dst is the same + # in both (eg. Europe/Warsaw 1915 when they switched to CET). + # At this point, we just have to guess unless we allow more + # hints to be passed in (such as the UTC offset or abbreviation), + # but that is just getting silly. + # + # Choose the earliest (by UTC) applicable timezone if is_dst=True + # Choose the latest (by UTC) applicable timezone if is_dst=False + # i.e., behave like end-of-DST transition + dates = {} # utc -> local + for local_dt in filtered_possible_loc_dt: + utc_time = ( + local_dt.replace(tzinfo=None) - local_dt.tzinfo._utcoffset) + assert utc_time not in dates + dates[utc_time] = local_dt + return dates[[min, max][not is_dst](dates)] + + def utcoffset(self, dt, is_dst=None): + '''See datetime.tzinfo.utcoffset + + The is_dst parameter may be used to remove ambiguity during DST + transitions. + + >>> from pytz import timezone + >>> tz = timezone('America/St_Johns') + >>> ambiguous = datetime(2009, 10, 31, 23, 30) + + >>> str(tz.utcoffset(ambiguous, is_dst=False)) + '-1 day, 20:30:00' + + >>> str(tz.utcoffset(ambiguous, is_dst=True)) + '-1 day, 21:30:00' + + >>> try: + ... tz.utcoffset(ambiguous) + ... except AmbiguousTimeError: + ... print('Ambiguous') + Ambiguous + + ''' + if dt is None: + return None + elif dt.tzinfo is not self: + dt = self.localize(dt, is_dst) + return dt.tzinfo._utcoffset + else: + return self._utcoffset + + def dst(self, dt, is_dst=None): + '''See datetime.tzinfo.dst + + The is_dst parameter may be used to remove ambiguity during DST + transitions. + + >>> from pytz import timezone + >>> tz = timezone('America/St_Johns') + + >>> normal = datetime(2009, 9, 1) + + >>> str(tz.dst(normal)) + '1:00:00' + >>> str(tz.dst(normal, is_dst=False)) + '1:00:00' + >>> str(tz.dst(normal, is_dst=True)) + '1:00:00' + + >>> ambiguous = datetime(2009, 10, 31, 23, 30) + + >>> str(tz.dst(ambiguous, is_dst=False)) + '0:00:00' + >>> str(tz.dst(ambiguous, is_dst=True)) + '1:00:00' + >>> try: + ... tz.dst(ambiguous) + ... except AmbiguousTimeError: + ... print('Ambiguous') + Ambiguous + + ''' + if dt is None: + return None + elif dt.tzinfo is not self: + dt = self.localize(dt, is_dst) + return dt.tzinfo._dst + else: + return self._dst + + def tzname(self, dt, is_dst=None): + '''See datetime.tzinfo.tzname + + The is_dst parameter may be used to remove ambiguity during DST + transitions. + + >>> from pytz import timezone + >>> tz = timezone('America/St_Johns') + + >>> normal = datetime(2009, 9, 1) + + >>> tz.tzname(normal) + 'NDT' + >>> tz.tzname(normal, is_dst=False) + 'NDT' + >>> tz.tzname(normal, is_dst=True) + 'NDT' + + >>> ambiguous = datetime(2009, 10, 31, 23, 30) + + >>> tz.tzname(ambiguous, is_dst=False) + 'NST' + >>> tz.tzname(ambiguous, is_dst=True) + 'NDT' + >>> try: + ... tz.tzname(ambiguous) + ... except AmbiguousTimeError: + ... print('Ambiguous') + Ambiguous + ''' + if dt is None: + return self.zone + elif dt.tzinfo is not self: + dt = self.localize(dt, is_dst) + return dt.tzinfo._tzname + else: + return self._tzname + + def __repr__(self): + if self._dst: + dst = 'DST' + else: + dst = 'STD' + if self._utcoffset > _notime: + return '' % ( + self.zone, self._tzname, self._utcoffset, dst + ) + else: + return '' % ( + self.zone, self._tzname, self._utcoffset, dst + ) + + def __reduce__(self): + # Special pickle to zone remains a singleton and to cope with + # database changes. + return pytz._p, ( + self.zone, + _to_seconds(self._utcoffset), + _to_seconds(self._dst), + self._tzname + ) + + +def unpickler(zone, utcoffset=None, dstoffset=None, tzname=None): + """Factory function for unpickling pytz tzinfo instances. + + This is shared for both StaticTzInfo and DstTzInfo instances, because + database changes could cause a zones implementation to switch between + these two base classes and we can't break pickles on a pytz version + upgrade. + """ + # Raises a KeyError if zone no longer exists, which should never happen + # and would be a bug. + tz = pytz.timezone(zone) + + # A StaticTzInfo - just return it + if utcoffset is None: + return tz + + # This pickle was created from a DstTzInfo. We need to + # determine which of the list of tzinfo instances for this zone + # to use in order to restore the state of any datetime instances using + # it correctly. + utcoffset = memorized_timedelta(utcoffset) + dstoffset = memorized_timedelta(dstoffset) + try: + return tz._tzinfos[(utcoffset, dstoffset, tzname)] + except KeyError: + # The particular state requested in this timezone no longer exists. + # This indicates a corrupt pickle, or the timezone database has been + # corrected violently enough to make this particular + # (utcoffset,dstoffset) no longer exist in the zone, or the + # abbreviation has been changed. + pass + + # See if we can find an entry differing only by tzname. Abbreviations + # get changed from the initial guess by the database maintainers to + # match reality when this information is discovered. + for localized_tz in tz._tzinfos.values(): + if (localized_tz._utcoffset == utcoffset and + localized_tz._dst == dstoffset): + return localized_tz + + # This (utcoffset, dstoffset) information has been removed from the + # zone. Add it back. This might occur when the database maintainers have + # corrected incorrect information. datetime instances using this + # incorrect information will continue to do so, exactly as they were + # before being pickled. This is purely an overly paranoid safety net - I + # doubt this will ever been needed in real life. + inf = (utcoffset, dstoffset, tzname) + tz._tzinfos[inf] = tz.__class__(inf, tz._tzinfos) + return tz._tzinfos[inf] diff --git a/libs/pytz/zoneinfo/Africa/Abidjan b/libs/pytz/zoneinfo/Africa/Abidjan new file mode 100644 index 00000000..65d19ec2 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Abidjan differ diff --git a/libs/pytz/zoneinfo/Africa/Accra b/libs/pytz/zoneinfo/Africa/Accra new file mode 100644 index 00000000..eaaa818f Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Accra differ diff --git a/libs/pytz/zoneinfo/Africa/Addis_Ababa b/libs/pytz/zoneinfo/Africa/Addis_Ababa new file mode 100644 index 00000000..6e19601f Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Addis_Ababa differ diff --git a/libs/pytz/zoneinfo/Africa/Algiers b/libs/pytz/zoneinfo/Africa/Algiers new file mode 100644 index 00000000..a5867a67 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Algiers differ diff --git a/libs/pytz/zoneinfo/Africa/Asmara b/libs/pytz/zoneinfo/Africa/Asmara new file mode 100644 index 00000000..6e19601f Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Asmara differ diff --git a/libs/pytz/zoneinfo/Africa/Asmera b/libs/pytz/zoneinfo/Africa/Asmera new file mode 100644 index 00000000..6e19601f Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Asmera differ diff --git a/libs/pytz/zoneinfo/Africa/Bamako b/libs/pytz/zoneinfo/Africa/Bamako new file mode 100644 index 00000000..65d19ec2 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Bamako differ diff --git a/libs/pytz/zoneinfo/Africa/Bangui b/libs/pytz/zoneinfo/Africa/Bangui new file mode 100644 index 00000000..cbdc0450 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Bangui differ diff --git a/libs/pytz/zoneinfo/Africa/Banjul b/libs/pytz/zoneinfo/Africa/Banjul new file mode 100644 index 00000000..65d19ec2 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Banjul differ diff --git a/libs/pytz/zoneinfo/Africa/Bissau b/libs/pytz/zoneinfo/Africa/Bissau new file mode 100644 index 00000000..82ea5aaf Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Bissau differ diff --git a/libs/pytz/zoneinfo/Africa/Blantyre b/libs/pytz/zoneinfo/Africa/Blantyre new file mode 100644 index 00000000..31cfad77 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Blantyre differ diff --git a/libs/pytz/zoneinfo/Africa/Brazzaville b/libs/pytz/zoneinfo/Africa/Brazzaville new file mode 100644 index 00000000..cbdc0450 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Brazzaville differ diff --git a/libs/pytz/zoneinfo/Africa/Bujumbura b/libs/pytz/zoneinfo/Africa/Bujumbura new file mode 100644 index 00000000..31cfad77 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Bujumbura differ diff --git a/libs/pytz/zoneinfo/Africa/Cairo b/libs/pytz/zoneinfo/Africa/Cairo new file mode 100644 index 00000000..0272fa1b Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Cairo differ diff --git a/libs/pytz/zoneinfo/Africa/Casablanca b/libs/pytz/zoneinfo/Africa/Casablanca new file mode 100644 index 00000000..04d16090 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Casablanca differ diff --git a/libs/pytz/zoneinfo/Africa/Ceuta b/libs/pytz/zoneinfo/Africa/Ceuta new file mode 100644 index 00000000..dd75e3e6 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Ceuta differ diff --git a/libs/pytz/zoneinfo/Africa/Conakry b/libs/pytz/zoneinfo/Africa/Conakry new file mode 100644 index 00000000..65d19ec2 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Conakry differ diff --git a/libs/pytz/zoneinfo/Africa/Dakar b/libs/pytz/zoneinfo/Africa/Dakar new file mode 100644 index 00000000..65d19ec2 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Dakar differ diff --git a/libs/pytz/zoneinfo/Africa/Dar_es_Salaam b/libs/pytz/zoneinfo/Africa/Dar_es_Salaam new file mode 100644 index 00000000..6e19601f Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Dar_es_Salaam differ diff --git a/libs/pytz/zoneinfo/Africa/Djibouti b/libs/pytz/zoneinfo/Africa/Djibouti new file mode 100644 index 00000000..6e19601f Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Djibouti differ diff --git a/libs/pytz/zoneinfo/Africa/Douala b/libs/pytz/zoneinfo/Africa/Douala new file mode 100644 index 00000000..cbdc0450 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Douala differ diff --git a/libs/pytz/zoneinfo/Africa/El_Aaiun b/libs/pytz/zoneinfo/Africa/El_Aaiun new file mode 100644 index 00000000..7272ec96 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/El_Aaiun differ diff --git a/libs/pytz/zoneinfo/Africa/Freetown b/libs/pytz/zoneinfo/Africa/Freetown new file mode 100644 index 00000000..65d19ec2 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Freetown differ diff --git a/libs/pytz/zoneinfo/Africa/Gaborone b/libs/pytz/zoneinfo/Africa/Gaborone new file mode 100644 index 00000000..31cfad77 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Gaborone differ diff --git a/libs/pytz/zoneinfo/Africa/Harare b/libs/pytz/zoneinfo/Africa/Harare new file mode 100644 index 00000000..31cfad77 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Harare differ diff --git a/libs/pytz/zoneinfo/Africa/Johannesburg b/libs/pytz/zoneinfo/Africa/Johannesburg new file mode 100644 index 00000000..b8b9270a Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Johannesburg differ diff --git a/libs/pytz/zoneinfo/Africa/Juba b/libs/pytz/zoneinfo/Africa/Juba new file mode 100644 index 00000000..83eca03a Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Juba differ diff --git a/libs/pytz/zoneinfo/Africa/Kampala b/libs/pytz/zoneinfo/Africa/Kampala new file mode 100644 index 00000000..6e19601f Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Kampala differ diff --git a/libs/pytz/zoneinfo/Africa/Khartoum b/libs/pytz/zoneinfo/Africa/Khartoum new file mode 100644 index 00000000..549dae27 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Khartoum differ diff --git a/libs/pytz/zoneinfo/Africa/Kigali b/libs/pytz/zoneinfo/Africa/Kigali new file mode 100644 index 00000000..31cfad77 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Kigali differ diff --git a/libs/pytz/zoneinfo/Africa/Kinshasa b/libs/pytz/zoneinfo/Africa/Kinshasa new file mode 100644 index 00000000..cbdc0450 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Kinshasa differ diff --git a/libs/pytz/zoneinfo/Africa/Lagos b/libs/pytz/zoneinfo/Africa/Lagos new file mode 100644 index 00000000..cbdc0450 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Lagos differ diff --git a/libs/pytz/zoneinfo/Africa/Libreville b/libs/pytz/zoneinfo/Africa/Libreville new file mode 100644 index 00000000..cbdc0450 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Libreville differ diff --git a/libs/pytz/zoneinfo/Africa/Lome b/libs/pytz/zoneinfo/Africa/Lome new file mode 100644 index 00000000..65d19ec2 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Lome differ diff --git a/libs/pytz/zoneinfo/Africa/Luanda b/libs/pytz/zoneinfo/Africa/Luanda new file mode 100644 index 00000000..cbdc0450 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Luanda differ diff --git a/libs/pytz/zoneinfo/Africa/Lubumbashi b/libs/pytz/zoneinfo/Africa/Lubumbashi new file mode 100644 index 00000000..31cfad77 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Lubumbashi differ diff --git a/libs/pytz/zoneinfo/Africa/Lusaka b/libs/pytz/zoneinfo/Africa/Lusaka new file mode 100644 index 00000000..31cfad77 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Lusaka differ diff --git a/libs/pytz/zoneinfo/Africa/Malabo b/libs/pytz/zoneinfo/Africa/Malabo new file mode 100644 index 00000000..cbdc0450 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Malabo differ diff --git a/libs/pytz/zoneinfo/Africa/Maputo b/libs/pytz/zoneinfo/Africa/Maputo new file mode 100644 index 00000000..31cfad77 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Maputo differ diff --git a/libs/pytz/zoneinfo/Africa/Maseru b/libs/pytz/zoneinfo/Africa/Maseru new file mode 100644 index 00000000..b8b9270a Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Maseru differ diff --git a/libs/pytz/zoneinfo/Africa/Mbabane b/libs/pytz/zoneinfo/Africa/Mbabane new file mode 100644 index 00000000..b8b9270a Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Mbabane differ diff --git a/libs/pytz/zoneinfo/Africa/Mogadishu b/libs/pytz/zoneinfo/Africa/Mogadishu new file mode 100644 index 00000000..6e19601f Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Mogadishu differ diff --git a/libs/pytz/zoneinfo/Africa/Monrovia b/libs/pytz/zoneinfo/Africa/Monrovia new file mode 100644 index 00000000..2a154f46 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Monrovia differ diff --git a/libs/pytz/zoneinfo/Africa/Nairobi b/libs/pytz/zoneinfo/Africa/Nairobi new file mode 100644 index 00000000..6e19601f Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Nairobi differ diff --git a/libs/pytz/zoneinfo/Africa/Ndjamena b/libs/pytz/zoneinfo/Africa/Ndjamena new file mode 100644 index 00000000..8779590e Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Ndjamena differ diff --git a/libs/pytz/zoneinfo/Africa/Niamey b/libs/pytz/zoneinfo/Africa/Niamey new file mode 100644 index 00000000..cbdc0450 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Niamey differ diff --git a/libs/pytz/zoneinfo/Africa/Nouakchott b/libs/pytz/zoneinfo/Africa/Nouakchott new file mode 100644 index 00000000..65d19ec2 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Nouakchott differ diff --git a/libs/pytz/zoneinfo/Africa/Ouagadougou b/libs/pytz/zoneinfo/Africa/Ouagadougou new file mode 100644 index 00000000..65d19ec2 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Ouagadougou differ diff --git a/libs/pytz/zoneinfo/Africa/Porto-Novo b/libs/pytz/zoneinfo/Africa/Porto-Novo new file mode 100644 index 00000000..cbdc0450 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Porto-Novo differ diff --git a/libs/pytz/zoneinfo/Africa/Sao_Tome b/libs/pytz/zoneinfo/Africa/Sao_Tome new file mode 100644 index 00000000..d2a64bd1 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Sao_Tome differ diff --git a/libs/pytz/zoneinfo/Africa/Timbuktu b/libs/pytz/zoneinfo/Africa/Timbuktu new file mode 100644 index 00000000..65d19ec2 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Timbuktu differ diff --git a/libs/pytz/zoneinfo/Africa/Tripoli b/libs/pytz/zoneinfo/Africa/Tripoli new file mode 100644 index 00000000..bd885315 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Tripoli differ diff --git a/libs/pytz/zoneinfo/Africa/Tunis b/libs/pytz/zoneinfo/Africa/Tunis new file mode 100644 index 00000000..0cd8ffba Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Tunis differ diff --git a/libs/pytz/zoneinfo/Africa/Windhoek b/libs/pytz/zoneinfo/Africa/Windhoek new file mode 100644 index 00000000..67661856 Binary files /dev/null and b/libs/pytz/zoneinfo/Africa/Windhoek differ diff --git a/libs/pytz/zoneinfo/America/Adak b/libs/pytz/zoneinfo/America/Adak new file mode 100644 index 00000000..43236498 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Adak differ diff --git a/libs/pytz/zoneinfo/America/Anchorage b/libs/pytz/zoneinfo/America/Anchorage new file mode 100644 index 00000000..9bbb2fd3 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Anchorage differ diff --git a/libs/pytz/zoneinfo/America/Anguilla b/libs/pytz/zoneinfo/America/Anguilla new file mode 100644 index 00000000..bdedd1bd Binary files /dev/null and b/libs/pytz/zoneinfo/America/Anguilla differ diff --git a/libs/pytz/zoneinfo/America/Antigua b/libs/pytz/zoneinfo/America/Antigua new file mode 100644 index 00000000..bdedd1bd Binary files /dev/null and b/libs/pytz/zoneinfo/America/Antigua differ diff --git a/libs/pytz/zoneinfo/America/Araguaina b/libs/pytz/zoneinfo/America/Araguaina new file mode 100644 index 00000000..bc9a5228 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Araguaina differ diff --git a/libs/pytz/zoneinfo/America/Argentina/Buenos_Aires b/libs/pytz/zoneinfo/America/Argentina/Buenos_Aires new file mode 100644 index 00000000..dfebfb99 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Argentina/Buenos_Aires differ diff --git a/libs/pytz/zoneinfo/America/Argentina/Catamarca b/libs/pytz/zoneinfo/America/Argentina/Catamarca new file mode 100644 index 00000000..b798105e Binary files /dev/null and b/libs/pytz/zoneinfo/America/Argentina/Catamarca differ diff --git a/libs/pytz/zoneinfo/America/Argentina/ComodRivadavia b/libs/pytz/zoneinfo/America/Argentina/ComodRivadavia new file mode 100644 index 00000000..b798105e Binary files /dev/null and b/libs/pytz/zoneinfo/America/Argentina/ComodRivadavia differ diff --git a/libs/pytz/zoneinfo/America/Argentina/Cordoba b/libs/pytz/zoneinfo/America/Argentina/Cordoba new file mode 100644 index 00000000..5df3cf6e Binary files /dev/null and b/libs/pytz/zoneinfo/America/Argentina/Cordoba differ diff --git a/libs/pytz/zoneinfo/America/Argentina/Jujuy b/libs/pytz/zoneinfo/America/Argentina/Jujuy new file mode 100644 index 00000000..7d2ba91c Binary files /dev/null and b/libs/pytz/zoneinfo/America/Argentina/Jujuy differ diff --git a/libs/pytz/zoneinfo/America/Argentina/La_Rioja b/libs/pytz/zoneinfo/America/Argentina/La_Rioja new file mode 100644 index 00000000..7654aebf Binary files /dev/null and b/libs/pytz/zoneinfo/America/Argentina/La_Rioja differ diff --git a/libs/pytz/zoneinfo/America/Argentina/Mendoza b/libs/pytz/zoneinfo/America/Argentina/Mendoza new file mode 100644 index 00000000..10323564 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Argentina/Mendoza differ diff --git a/libs/pytz/zoneinfo/America/Argentina/Rio_Gallegos b/libs/pytz/zoneinfo/America/Argentina/Rio_Gallegos new file mode 100644 index 00000000..3c849fce Binary files /dev/null and b/libs/pytz/zoneinfo/America/Argentina/Rio_Gallegos differ diff --git a/libs/pytz/zoneinfo/America/Argentina/Salta b/libs/pytz/zoneinfo/America/Argentina/Salta new file mode 100644 index 00000000..a4b71c1f Binary files /dev/null and b/libs/pytz/zoneinfo/America/Argentina/Salta differ diff --git a/libs/pytz/zoneinfo/America/Argentina/San_Juan b/libs/pytz/zoneinfo/America/Argentina/San_Juan new file mode 100644 index 00000000..948a3901 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Argentina/San_Juan differ diff --git a/libs/pytz/zoneinfo/America/Argentina/San_Luis b/libs/pytz/zoneinfo/America/Argentina/San_Luis new file mode 100644 index 00000000..acfbbe43 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Argentina/San_Luis differ diff --git a/libs/pytz/zoneinfo/America/Argentina/Tucuman b/libs/pytz/zoneinfo/America/Argentina/Tucuman new file mode 100644 index 00000000..085fc9cc Binary files /dev/null and b/libs/pytz/zoneinfo/America/Argentina/Tucuman differ diff --git a/libs/pytz/zoneinfo/America/Argentina/Ushuaia b/libs/pytz/zoneinfo/America/Argentina/Ushuaia new file mode 100644 index 00000000..1fc32567 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Argentina/Ushuaia differ diff --git a/libs/pytz/zoneinfo/America/Aruba b/libs/pytz/zoneinfo/America/Aruba new file mode 100644 index 00000000..d3b318d2 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Aruba differ diff --git a/libs/pytz/zoneinfo/America/Asuncion b/libs/pytz/zoneinfo/America/Asuncion new file mode 100644 index 00000000..831bf843 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Asuncion differ diff --git a/libs/pytz/zoneinfo/America/Atikokan b/libs/pytz/zoneinfo/America/Atikokan new file mode 100644 index 00000000..629ed423 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Atikokan differ diff --git a/libs/pytz/zoneinfo/America/Atka b/libs/pytz/zoneinfo/America/Atka new file mode 100644 index 00000000..43236498 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Atka differ diff --git a/libs/pytz/zoneinfo/America/Bahia b/libs/pytz/zoneinfo/America/Bahia new file mode 100644 index 00000000..143eafc2 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Bahia differ diff --git a/libs/pytz/zoneinfo/America/Bahia_Banderas b/libs/pytz/zoneinfo/America/Bahia_Banderas new file mode 100644 index 00000000..cd531078 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Bahia_Banderas differ diff --git a/libs/pytz/zoneinfo/America/Barbados b/libs/pytz/zoneinfo/America/Barbados new file mode 100644 index 00000000..7bb7ac4d Binary files /dev/null and b/libs/pytz/zoneinfo/America/Barbados differ diff --git a/libs/pytz/zoneinfo/America/Belem b/libs/pytz/zoneinfo/America/Belem new file mode 100644 index 00000000..ab3c8a67 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Belem differ diff --git a/libs/pytz/zoneinfo/America/Belize b/libs/pytz/zoneinfo/America/Belize new file mode 100644 index 00000000..fd693214 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Belize differ diff --git a/libs/pytz/zoneinfo/America/Blanc-Sablon b/libs/pytz/zoneinfo/America/Blanc-Sablon new file mode 100644 index 00000000..f9f13a16 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Blanc-Sablon differ diff --git a/libs/pytz/zoneinfo/America/Boa_Vista b/libs/pytz/zoneinfo/America/Boa_Vista new file mode 100644 index 00000000..69e17a00 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Boa_Vista differ diff --git a/libs/pytz/zoneinfo/America/Bogota b/libs/pytz/zoneinfo/America/Bogota new file mode 100644 index 00000000..b7ded8e6 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Bogota differ diff --git a/libs/pytz/zoneinfo/America/Boise b/libs/pytz/zoneinfo/America/Boise new file mode 100644 index 00000000..f8d54e27 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Boise differ diff --git a/libs/pytz/zoneinfo/America/Buenos_Aires b/libs/pytz/zoneinfo/America/Buenos_Aires new file mode 100644 index 00000000..dfebfb99 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Buenos_Aires differ diff --git a/libs/pytz/zoneinfo/America/Cambridge_Bay b/libs/pytz/zoneinfo/America/Cambridge_Bay new file mode 100644 index 00000000..f8db4b6e Binary files /dev/null and b/libs/pytz/zoneinfo/America/Cambridge_Bay differ diff --git a/libs/pytz/zoneinfo/America/Campo_Grande b/libs/pytz/zoneinfo/America/Campo_Grande new file mode 100644 index 00000000..495ef456 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Campo_Grande differ diff --git a/libs/pytz/zoneinfo/America/Cancun b/libs/pytz/zoneinfo/America/Cancun new file mode 100644 index 00000000..de6930cd Binary files /dev/null and b/libs/pytz/zoneinfo/America/Cancun differ diff --git a/libs/pytz/zoneinfo/America/Caracas b/libs/pytz/zoneinfo/America/Caracas new file mode 100644 index 00000000..9abd028f Binary files /dev/null and b/libs/pytz/zoneinfo/America/Caracas differ diff --git a/libs/pytz/zoneinfo/America/Catamarca b/libs/pytz/zoneinfo/America/Catamarca new file mode 100644 index 00000000..b798105e Binary files /dev/null and b/libs/pytz/zoneinfo/America/Catamarca differ diff --git a/libs/pytz/zoneinfo/America/Cayenne b/libs/pytz/zoneinfo/America/Cayenne new file mode 100644 index 00000000..ff596578 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Cayenne differ diff --git a/libs/pytz/zoneinfo/America/Cayman b/libs/pytz/zoneinfo/America/Cayman new file mode 100644 index 00000000..55b08346 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Cayman differ diff --git a/libs/pytz/zoneinfo/America/Chicago b/libs/pytz/zoneinfo/America/Chicago new file mode 100644 index 00000000..a5b1617c Binary files /dev/null and b/libs/pytz/zoneinfo/America/Chicago differ diff --git a/libs/pytz/zoneinfo/America/Chihuahua b/libs/pytz/zoneinfo/America/Chihuahua new file mode 100644 index 00000000..b2687241 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Chihuahua differ diff --git a/libs/pytz/zoneinfo/America/Coral_Harbour b/libs/pytz/zoneinfo/America/Coral_Harbour new file mode 100644 index 00000000..629ed423 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Coral_Harbour differ diff --git a/libs/pytz/zoneinfo/America/Cordoba b/libs/pytz/zoneinfo/America/Cordoba new file mode 100644 index 00000000..5df3cf6e Binary files /dev/null and b/libs/pytz/zoneinfo/America/Cordoba differ diff --git a/libs/pytz/zoneinfo/America/Costa_Rica b/libs/pytz/zoneinfo/America/Costa_Rica new file mode 100644 index 00000000..525a67ea Binary files /dev/null and b/libs/pytz/zoneinfo/America/Costa_Rica differ diff --git a/libs/pytz/zoneinfo/America/Creston b/libs/pytz/zoneinfo/America/Creston new file mode 100644 index 00000000..0fba7417 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Creston differ diff --git a/libs/pytz/zoneinfo/America/Cuiaba b/libs/pytz/zoneinfo/America/Cuiaba new file mode 100644 index 00000000..8a4ee7d0 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Cuiaba differ diff --git a/libs/pytz/zoneinfo/America/Curacao b/libs/pytz/zoneinfo/America/Curacao new file mode 100644 index 00000000..d3b318d2 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Curacao differ diff --git a/libs/pytz/zoneinfo/America/Danmarkshavn b/libs/pytz/zoneinfo/America/Danmarkshavn new file mode 100644 index 00000000..9549adcb Binary files /dev/null and b/libs/pytz/zoneinfo/America/Danmarkshavn differ diff --git a/libs/pytz/zoneinfo/America/Dawson b/libs/pytz/zoneinfo/America/Dawson new file mode 100644 index 00000000..db9ceadd Binary files /dev/null and b/libs/pytz/zoneinfo/America/Dawson differ diff --git a/libs/pytz/zoneinfo/America/Dawson_Creek b/libs/pytz/zoneinfo/America/Dawson_Creek new file mode 100644 index 00000000..db9e3396 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Dawson_Creek differ diff --git a/libs/pytz/zoneinfo/America/Denver b/libs/pytz/zoneinfo/America/Denver new file mode 100644 index 00000000..5fbe26b1 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Denver differ diff --git a/libs/pytz/zoneinfo/America/Detroit b/libs/pytz/zoneinfo/America/Detroit new file mode 100644 index 00000000..5e022605 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Detroit differ diff --git a/libs/pytz/zoneinfo/America/Dominica b/libs/pytz/zoneinfo/America/Dominica new file mode 100644 index 00000000..bdedd1bd Binary files /dev/null and b/libs/pytz/zoneinfo/America/Dominica differ diff --git a/libs/pytz/zoneinfo/America/Edmonton b/libs/pytz/zoneinfo/America/Edmonton new file mode 100644 index 00000000..3fa05798 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Edmonton differ diff --git a/libs/pytz/zoneinfo/America/Eirunepe b/libs/pytz/zoneinfo/America/Eirunepe new file mode 100644 index 00000000..99b7d06e Binary files /dev/null and b/libs/pytz/zoneinfo/America/Eirunepe differ diff --git a/libs/pytz/zoneinfo/America/El_Salvador b/libs/pytz/zoneinfo/America/El_Salvador new file mode 100644 index 00000000..ac774e83 Binary files /dev/null and b/libs/pytz/zoneinfo/America/El_Salvador differ diff --git a/libs/pytz/zoneinfo/America/Ensenada b/libs/pytz/zoneinfo/America/Ensenada new file mode 100644 index 00000000..ada6bf78 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Ensenada differ diff --git a/libs/pytz/zoneinfo/America/Fort_Nelson b/libs/pytz/zoneinfo/America/Fort_Nelson new file mode 100644 index 00000000..5a0b7f1c Binary files /dev/null and b/libs/pytz/zoneinfo/America/Fort_Nelson differ diff --git a/libs/pytz/zoneinfo/America/Fort_Wayne b/libs/pytz/zoneinfo/America/Fort_Wayne new file mode 100644 index 00000000..09511ccd Binary files /dev/null and b/libs/pytz/zoneinfo/America/Fort_Wayne differ diff --git a/libs/pytz/zoneinfo/America/Fortaleza b/libs/pytz/zoneinfo/America/Fortaleza new file mode 100644 index 00000000..e637170a Binary files /dev/null and b/libs/pytz/zoneinfo/America/Fortaleza differ diff --git a/libs/pytz/zoneinfo/America/Glace_Bay b/libs/pytz/zoneinfo/America/Glace_Bay new file mode 100644 index 00000000..48412a4c Binary files /dev/null and b/libs/pytz/zoneinfo/America/Glace_Bay differ diff --git a/libs/pytz/zoneinfo/America/Godthab b/libs/pytz/zoneinfo/America/Godthab new file mode 100644 index 00000000..0160308b Binary files /dev/null and b/libs/pytz/zoneinfo/America/Godthab differ diff --git a/libs/pytz/zoneinfo/America/Goose_Bay b/libs/pytz/zoneinfo/America/Goose_Bay new file mode 100644 index 00000000..a3f29907 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Goose_Bay differ diff --git a/libs/pytz/zoneinfo/America/Grand_Turk b/libs/pytz/zoneinfo/America/Grand_Turk new file mode 100644 index 00000000..4597a621 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Grand_Turk differ diff --git a/libs/pytz/zoneinfo/America/Grenada b/libs/pytz/zoneinfo/America/Grenada new file mode 100644 index 00000000..bdedd1bd Binary files /dev/null and b/libs/pytz/zoneinfo/America/Grenada differ diff --git a/libs/pytz/zoneinfo/America/Guadeloupe b/libs/pytz/zoneinfo/America/Guadeloupe new file mode 100644 index 00000000..bdedd1bd Binary files /dev/null and b/libs/pytz/zoneinfo/America/Guadeloupe differ diff --git a/libs/pytz/zoneinfo/America/Guatemala b/libs/pytz/zoneinfo/America/Guatemala new file mode 100644 index 00000000..6118b5ce Binary files /dev/null and b/libs/pytz/zoneinfo/America/Guatemala differ diff --git a/libs/pytz/zoneinfo/America/Guayaquil b/libs/pytz/zoneinfo/America/Guayaquil new file mode 100644 index 00000000..bf087a0e Binary files /dev/null and b/libs/pytz/zoneinfo/America/Guayaquil differ diff --git a/libs/pytz/zoneinfo/America/Guyana b/libs/pytz/zoneinfo/America/Guyana new file mode 100644 index 00000000..d1dd2faf Binary files /dev/null and b/libs/pytz/zoneinfo/America/Guyana differ diff --git a/libs/pytz/zoneinfo/America/Halifax b/libs/pytz/zoneinfo/America/Halifax new file mode 100644 index 00000000..756099ab Binary files /dev/null and b/libs/pytz/zoneinfo/America/Halifax differ diff --git a/libs/pytz/zoneinfo/America/Havana b/libs/pytz/zoneinfo/America/Havana new file mode 100644 index 00000000..8186060a Binary files /dev/null and b/libs/pytz/zoneinfo/America/Havana differ diff --git a/libs/pytz/zoneinfo/America/Hermosillo b/libs/pytz/zoneinfo/America/Hermosillo new file mode 100644 index 00000000..26c269d9 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Hermosillo differ diff --git a/libs/pytz/zoneinfo/America/Indiana/Indianapolis b/libs/pytz/zoneinfo/America/Indiana/Indianapolis new file mode 100644 index 00000000..09511ccd Binary files /dev/null and b/libs/pytz/zoneinfo/America/Indiana/Indianapolis differ diff --git a/libs/pytz/zoneinfo/America/Indiana/Knox b/libs/pytz/zoneinfo/America/Indiana/Knox new file mode 100644 index 00000000..fcd408d7 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Indiana/Knox differ diff --git a/libs/pytz/zoneinfo/America/Indiana/Marengo b/libs/pytz/zoneinfo/America/Indiana/Marengo new file mode 100644 index 00000000..1abf75e7 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Indiana/Marengo differ diff --git a/libs/pytz/zoneinfo/America/Indiana/Petersburg b/libs/pytz/zoneinfo/America/Indiana/Petersburg new file mode 100644 index 00000000..0133548e Binary files /dev/null and b/libs/pytz/zoneinfo/America/Indiana/Petersburg differ diff --git a/libs/pytz/zoneinfo/America/Indiana/Tell_City b/libs/pytz/zoneinfo/America/Indiana/Tell_City new file mode 100644 index 00000000..4ce95c15 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Indiana/Tell_City differ diff --git a/libs/pytz/zoneinfo/America/Indiana/Vevay b/libs/pytz/zoneinfo/America/Indiana/Vevay new file mode 100644 index 00000000..d236b7c0 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Indiana/Vevay differ diff --git a/libs/pytz/zoneinfo/America/Indiana/Vincennes b/libs/pytz/zoneinfo/America/Indiana/Vincennes new file mode 100644 index 00000000..c818929d Binary files /dev/null and b/libs/pytz/zoneinfo/America/Indiana/Vincennes differ diff --git a/libs/pytz/zoneinfo/America/Indiana/Winamac b/libs/pytz/zoneinfo/America/Indiana/Winamac new file mode 100644 index 00000000..630935c1 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Indiana/Winamac differ diff --git a/libs/pytz/zoneinfo/America/Indianapolis b/libs/pytz/zoneinfo/America/Indianapolis new file mode 100644 index 00000000..09511ccd Binary files /dev/null and b/libs/pytz/zoneinfo/America/Indianapolis differ diff --git a/libs/pytz/zoneinfo/America/Inuvik b/libs/pytz/zoneinfo/America/Inuvik new file mode 100644 index 00000000..e107dc44 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Inuvik differ diff --git a/libs/pytz/zoneinfo/America/Iqaluit b/libs/pytz/zoneinfo/America/Iqaluit new file mode 100644 index 00000000..c8138bdb Binary files /dev/null and b/libs/pytz/zoneinfo/America/Iqaluit differ diff --git a/libs/pytz/zoneinfo/America/Jamaica b/libs/pytz/zoneinfo/America/Jamaica new file mode 100644 index 00000000..162306f8 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Jamaica differ diff --git a/libs/pytz/zoneinfo/America/Jujuy b/libs/pytz/zoneinfo/America/Jujuy new file mode 100644 index 00000000..7d2ba91c Binary files /dev/null and b/libs/pytz/zoneinfo/America/Jujuy differ diff --git a/libs/pytz/zoneinfo/America/Juneau b/libs/pytz/zoneinfo/America/Juneau new file mode 100644 index 00000000..451f3490 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Juneau differ diff --git a/libs/pytz/zoneinfo/America/Kentucky/Louisville b/libs/pytz/zoneinfo/America/Kentucky/Louisville new file mode 100644 index 00000000..f4c4cf96 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Kentucky/Louisville differ diff --git a/libs/pytz/zoneinfo/America/Kentucky/Monticello b/libs/pytz/zoneinfo/America/Kentucky/Monticello new file mode 100644 index 00000000..438e3eab Binary files /dev/null and b/libs/pytz/zoneinfo/America/Kentucky/Monticello differ diff --git a/libs/pytz/zoneinfo/America/Knox_IN b/libs/pytz/zoneinfo/America/Knox_IN new file mode 100644 index 00000000..fcd408d7 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Knox_IN differ diff --git a/libs/pytz/zoneinfo/America/Kralendijk b/libs/pytz/zoneinfo/America/Kralendijk new file mode 100644 index 00000000..d3b318d2 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Kralendijk differ diff --git a/libs/pytz/zoneinfo/America/La_Paz b/libs/pytz/zoneinfo/America/La_Paz new file mode 100644 index 00000000..5e5fec56 Binary files /dev/null and b/libs/pytz/zoneinfo/America/La_Paz differ diff --git a/libs/pytz/zoneinfo/America/Lima b/libs/pytz/zoneinfo/America/Lima new file mode 100644 index 00000000..d9fec371 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Lima differ diff --git a/libs/pytz/zoneinfo/America/Los_Angeles b/libs/pytz/zoneinfo/America/Los_Angeles new file mode 100644 index 00000000..9dad4f4c Binary files /dev/null and b/libs/pytz/zoneinfo/America/Los_Angeles differ diff --git a/libs/pytz/zoneinfo/America/Louisville b/libs/pytz/zoneinfo/America/Louisville new file mode 100644 index 00000000..f4c4cf96 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Louisville differ diff --git a/libs/pytz/zoneinfo/America/Lower_Princes b/libs/pytz/zoneinfo/America/Lower_Princes new file mode 100644 index 00000000..d3b318d2 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Lower_Princes differ diff --git a/libs/pytz/zoneinfo/America/Maceio b/libs/pytz/zoneinfo/America/Maceio new file mode 100644 index 00000000..fec8a8bf Binary files /dev/null and b/libs/pytz/zoneinfo/America/Maceio differ diff --git a/libs/pytz/zoneinfo/America/Managua b/libs/pytz/zoneinfo/America/Managua new file mode 100644 index 00000000..69256c63 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Managua differ diff --git a/libs/pytz/zoneinfo/America/Manaus b/libs/pytz/zoneinfo/America/Manaus new file mode 100644 index 00000000..b10241e6 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Manaus differ diff --git a/libs/pytz/zoneinfo/America/Marigot b/libs/pytz/zoneinfo/America/Marigot new file mode 100644 index 00000000..bdedd1bd Binary files /dev/null and b/libs/pytz/zoneinfo/America/Marigot differ diff --git a/libs/pytz/zoneinfo/America/Martinique b/libs/pytz/zoneinfo/America/Martinique new file mode 100644 index 00000000..79716de5 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Martinique differ diff --git a/libs/pytz/zoneinfo/America/Matamoros b/libs/pytz/zoneinfo/America/Matamoros new file mode 100644 index 00000000..5c59984d Binary files /dev/null and b/libs/pytz/zoneinfo/America/Matamoros differ diff --git a/libs/pytz/zoneinfo/America/Mazatlan b/libs/pytz/zoneinfo/America/Mazatlan new file mode 100644 index 00000000..43ee12d8 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Mazatlan differ diff --git a/libs/pytz/zoneinfo/America/Mendoza b/libs/pytz/zoneinfo/America/Mendoza new file mode 100644 index 00000000..10323564 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Mendoza differ diff --git a/libs/pytz/zoneinfo/America/Menominee b/libs/pytz/zoneinfo/America/Menominee new file mode 100644 index 00000000..31461386 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Menominee differ diff --git a/libs/pytz/zoneinfo/America/Merida b/libs/pytz/zoneinfo/America/Merida new file mode 100644 index 00000000..b46298e1 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Merida differ diff --git a/libs/pytz/zoneinfo/America/Metlakatla b/libs/pytz/zoneinfo/America/Metlakatla new file mode 100644 index 00000000..26356078 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Metlakatla differ diff --git a/libs/pytz/zoneinfo/America/Mexico_City b/libs/pytz/zoneinfo/America/Mexico_City new file mode 100644 index 00000000..1434ab08 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Mexico_City differ diff --git a/libs/pytz/zoneinfo/America/Miquelon b/libs/pytz/zoneinfo/America/Miquelon new file mode 100644 index 00000000..06ceaadf Binary files /dev/null and b/libs/pytz/zoneinfo/America/Miquelon differ diff --git a/libs/pytz/zoneinfo/America/Moncton b/libs/pytz/zoneinfo/America/Moncton new file mode 100644 index 00000000..9df8d0f2 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Moncton differ diff --git a/libs/pytz/zoneinfo/America/Monterrey b/libs/pytz/zoneinfo/America/Monterrey new file mode 100644 index 00000000..7dc50577 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Monterrey differ diff --git a/libs/pytz/zoneinfo/America/Montevideo b/libs/pytz/zoneinfo/America/Montevideo new file mode 100644 index 00000000..0d1e565c Binary files /dev/null and b/libs/pytz/zoneinfo/America/Montevideo differ diff --git a/libs/pytz/zoneinfo/America/Montreal b/libs/pytz/zoneinfo/America/Montreal new file mode 100644 index 00000000..6752c5b0 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Montreal differ diff --git a/libs/pytz/zoneinfo/America/Montserrat b/libs/pytz/zoneinfo/America/Montserrat new file mode 100644 index 00000000..bdedd1bd Binary files /dev/null and b/libs/pytz/zoneinfo/America/Montserrat differ diff --git a/libs/pytz/zoneinfo/America/Nassau b/libs/pytz/zoneinfo/America/Nassau new file mode 100644 index 00000000..5091eb5d Binary files /dev/null and b/libs/pytz/zoneinfo/America/Nassau differ diff --git a/libs/pytz/zoneinfo/America/New_York b/libs/pytz/zoneinfo/America/New_York new file mode 100644 index 00000000..2f75480e Binary files /dev/null and b/libs/pytz/zoneinfo/America/New_York differ diff --git a/libs/pytz/zoneinfo/America/Nipigon b/libs/pytz/zoneinfo/America/Nipigon new file mode 100644 index 00000000..f6a856e6 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Nipigon differ diff --git a/libs/pytz/zoneinfo/America/Nome b/libs/pytz/zoneinfo/America/Nome new file mode 100644 index 00000000..10998df3 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Nome differ diff --git a/libs/pytz/zoneinfo/America/Noronha b/libs/pytz/zoneinfo/America/Noronha new file mode 100644 index 00000000..95ff8a25 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Noronha differ diff --git a/libs/pytz/zoneinfo/America/North_Dakota/Beulah b/libs/pytz/zoneinfo/America/North_Dakota/Beulah new file mode 100644 index 00000000..246345dd Binary files /dev/null and b/libs/pytz/zoneinfo/America/North_Dakota/Beulah differ diff --git a/libs/pytz/zoneinfo/America/North_Dakota/Center b/libs/pytz/zoneinfo/America/North_Dakota/Center new file mode 100644 index 00000000..1fa07037 Binary files /dev/null and b/libs/pytz/zoneinfo/America/North_Dakota/Center differ diff --git a/libs/pytz/zoneinfo/America/North_Dakota/New_Salem b/libs/pytz/zoneinfo/America/North_Dakota/New_Salem new file mode 100644 index 00000000..123f2aee Binary files /dev/null and b/libs/pytz/zoneinfo/America/North_Dakota/New_Salem differ diff --git a/libs/pytz/zoneinfo/America/Ojinaga b/libs/pytz/zoneinfo/America/Ojinaga new file mode 100644 index 00000000..37d78301 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Ojinaga differ diff --git a/libs/pytz/zoneinfo/America/Panama b/libs/pytz/zoneinfo/America/Panama new file mode 100644 index 00000000..55b08346 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Panama differ diff --git a/libs/pytz/zoneinfo/America/Pangnirtung b/libs/pytz/zoneinfo/America/Pangnirtung new file mode 100644 index 00000000..3e4e0db6 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Pangnirtung differ diff --git a/libs/pytz/zoneinfo/America/Paramaribo b/libs/pytz/zoneinfo/America/Paramaribo new file mode 100644 index 00000000..b95c7842 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Paramaribo differ diff --git a/libs/pytz/zoneinfo/America/Phoenix b/libs/pytz/zoneinfo/America/Phoenix new file mode 100644 index 00000000..4d51271a Binary files /dev/null and b/libs/pytz/zoneinfo/America/Phoenix differ diff --git a/libs/pytz/zoneinfo/America/Port-au-Prince b/libs/pytz/zoneinfo/America/Port-au-Prince new file mode 100644 index 00000000..d9590103 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Port-au-Prince differ diff --git a/libs/pytz/zoneinfo/America/Port_of_Spain b/libs/pytz/zoneinfo/America/Port_of_Spain new file mode 100644 index 00000000..bdedd1bd Binary files /dev/null and b/libs/pytz/zoneinfo/America/Port_of_Spain differ diff --git a/libs/pytz/zoneinfo/America/Porto_Acre b/libs/pytz/zoneinfo/America/Porto_Acre new file mode 100644 index 00000000..16b7f923 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Porto_Acre differ diff --git a/libs/pytz/zoneinfo/America/Porto_Velho b/libs/pytz/zoneinfo/America/Porto_Velho new file mode 100644 index 00000000..10cb02b8 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Porto_Velho differ diff --git a/libs/pytz/zoneinfo/America/Puerto_Rico b/libs/pytz/zoneinfo/America/Puerto_Rico new file mode 100644 index 00000000..a662a571 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Puerto_Rico differ diff --git a/libs/pytz/zoneinfo/America/Punta_Arenas b/libs/pytz/zoneinfo/America/Punta_Arenas new file mode 100644 index 00000000..a5a8af52 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Punta_Arenas differ diff --git a/libs/pytz/zoneinfo/America/Rainy_River b/libs/pytz/zoneinfo/America/Rainy_River new file mode 100644 index 00000000..ea660991 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Rainy_River differ diff --git a/libs/pytz/zoneinfo/America/Rankin_Inlet b/libs/pytz/zoneinfo/America/Rankin_Inlet new file mode 100644 index 00000000..61ff6fcb Binary files /dev/null and b/libs/pytz/zoneinfo/America/Rankin_Inlet differ diff --git a/libs/pytz/zoneinfo/America/Recife b/libs/pytz/zoneinfo/America/Recife new file mode 100644 index 00000000..c6d99b3a Binary files /dev/null and b/libs/pytz/zoneinfo/America/Recife differ diff --git a/libs/pytz/zoneinfo/America/Regina b/libs/pytz/zoneinfo/America/Regina new file mode 100644 index 00000000..20c9c84d Binary files /dev/null and b/libs/pytz/zoneinfo/America/Regina differ diff --git a/libs/pytz/zoneinfo/America/Resolute b/libs/pytz/zoneinfo/America/Resolute new file mode 100644 index 00000000..4365a5c8 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Resolute differ diff --git a/libs/pytz/zoneinfo/America/Rio_Branco b/libs/pytz/zoneinfo/America/Rio_Branco new file mode 100644 index 00000000..16b7f923 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Rio_Branco differ diff --git a/libs/pytz/zoneinfo/America/Rosario b/libs/pytz/zoneinfo/America/Rosario new file mode 100644 index 00000000..5df3cf6e Binary files /dev/null and b/libs/pytz/zoneinfo/America/Rosario differ diff --git a/libs/pytz/zoneinfo/America/Santa_Isabel b/libs/pytz/zoneinfo/America/Santa_Isabel new file mode 100644 index 00000000..ada6bf78 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Santa_Isabel differ diff --git a/libs/pytz/zoneinfo/America/Santarem b/libs/pytz/zoneinfo/America/Santarem new file mode 100644 index 00000000..8080efab Binary files /dev/null and b/libs/pytz/zoneinfo/America/Santarem differ diff --git a/libs/pytz/zoneinfo/America/Santiago b/libs/pytz/zoneinfo/America/Santiago new file mode 100644 index 00000000..816a0428 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Santiago differ diff --git a/libs/pytz/zoneinfo/America/Santo_Domingo b/libs/pytz/zoneinfo/America/Santo_Domingo new file mode 100644 index 00000000..4e5eba52 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Santo_Domingo differ diff --git a/libs/pytz/zoneinfo/America/Sao_Paulo b/libs/pytz/zoneinfo/America/Sao_Paulo new file mode 100644 index 00000000..c417ba1d Binary files /dev/null and b/libs/pytz/zoneinfo/America/Sao_Paulo differ diff --git a/libs/pytz/zoneinfo/America/Scoresbysund b/libs/pytz/zoneinfo/America/Scoresbysund new file mode 100644 index 00000000..e20e9e1c Binary files /dev/null and b/libs/pytz/zoneinfo/America/Scoresbysund differ diff --git a/libs/pytz/zoneinfo/America/Shiprock b/libs/pytz/zoneinfo/America/Shiprock new file mode 100644 index 00000000..5fbe26b1 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Shiprock differ diff --git a/libs/pytz/zoneinfo/America/Sitka b/libs/pytz/zoneinfo/America/Sitka new file mode 100644 index 00000000..31f70613 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Sitka differ diff --git a/libs/pytz/zoneinfo/America/St_Barthelemy b/libs/pytz/zoneinfo/America/St_Barthelemy new file mode 100644 index 00000000..bdedd1bd Binary files /dev/null and b/libs/pytz/zoneinfo/America/St_Barthelemy differ diff --git a/libs/pytz/zoneinfo/America/St_Johns b/libs/pytz/zoneinfo/America/St_Johns new file mode 100644 index 00000000..65a5b0c7 Binary files /dev/null and b/libs/pytz/zoneinfo/America/St_Johns differ diff --git a/libs/pytz/zoneinfo/America/St_Kitts b/libs/pytz/zoneinfo/America/St_Kitts new file mode 100644 index 00000000..bdedd1bd Binary files /dev/null and b/libs/pytz/zoneinfo/America/St_Kitts differ diff --git a/libs/pytz/zoneinfo/America/St_Lucia b/libs/pytz/zoneinfo/America/St_Lucia new file mode 100644 index 00000000..bdedd1bd Binary files /dev/null and b/libs/pytz/zoneinfo/America/St_Lucia differ diff --git a/libs/pytz/zoneinfo/America/St_Thomas b/libs/pytz/zoneinfo/America/St_Thomas new file mode 100644 index 00000000..bdedd1bd Binary files /dev/null and b/libs/pytz/zoneinfo/America/St_Thomas differ diff --git a/libs/pytz/zoneinfo/America/St_Vincent b/libs/pytz/zoneinfo/America/St_Vincent new file mode 100644 index 00000000..bdedd1bd Binary files /dev/null and b/libs/pytz/zoneinfo/America/St_Vincent differ diff --git a/libs/pytz/zoneinfo/America/Swift_Current b/libs/pytz/zoneinfo/America/Swift_Current new file mode 100644 index 00000000..8e9ef255 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Swift_Current differ diff --git a/libs/pytz/zoneinfo/America/Tegucigalpa b/libs/pytz/zoneinfo/America/Tegucigalpa new file mode 100644 index 00000000..477e9395 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Tegucigalpa differ diff --git a/libs/pytz/zoneinfo/America/Thule b/libs/pytz/zoneinfo/America/Thule new file mode 100644 index 00000000..2969ebe5 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Thule differ diff --git a/libs/pytz/zoneinfo/America/Thunder_Bay b/libs/pytz/zoneinfo/America/Thunder_Bay new file mode 100644 index 00000000..e504c9ac Binary files /dev/null and b/libs/pytz/zoneinfo/America/Thunder_Bay differ diff --git a/libs/pytz/zoneinfo/America/Tijuana b/libs/pytz/zoneinfo/America/Tijuana new file mode 100644 index 00000000..ada6bf78 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Tijuana differ diff --git a/libs/pytz/zoneinfo/America/Toronto b/libs/pytz/zoneinfo/America/Toronto new file mode 100644 index 00000000..6752c5b0 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Toronto differ diff --git a/libs/pytz/zoneinfo/America/Tortola b/libs/pytz/zoneinfo/America/Tortola new file mode 100644 index 00000000..bdedd1bd Binary files /dev/null and b/libs/pytz/zoneinfo/America/Tortola differ diff --git a/libs/pytz/zoneinfo/America/Vancouver b/libs/pytz/zoneinfo/America/Vancouver new file mode 100644 index 00000000..0f9f8328 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Vancouver differ diff --git a/libs/pytz/zoneinfo/America/Virgin b/libs/pytz/zoneinfo/America/Virgin new file mode 100644 index 00000000..bdedd1bd Binary files /dev/null and b/libs/pytz/zoneinfo/America/Virgin differ diff --git a/libs/pytz/zoneinfo/America/Whitehorse b/libs/pytz/zoneinfo/America/Whitehorse new file mode 100644 index 00000000..fb3cd71a Binary files /dev/null and b/libs/pytz/zoneinfo/America/Whitehorse differ diff --git a/libs/pytz/zoneinfo/America/Winnipeg b/libs/pytz/zoneinfo/America/Winnipeg new file mode 100644 index 00000000..3718d47d Binary files /dev/null and b/libs/pytz/zoneinfo/America/Winnipeg differ diff --git a/libs/pytz/zoneinfo/America/Yakutat b/libs/pytz/zoneinfo/America/Yakutat new file mode 100644 index 00000000..da209f9f Binary files /dev/null and b/libs/pytz/zoneinfo/America/Yakutat differ diff --git a/libs/pytz/zoneinfo/America/Yellowknife b/libs/pytz/zoneinfo/America/Yellowknife new file mode 100644 index 00000000..e6afa390 Binary files /dev/null and b/libs/pytz/zoneinfo/America/Yellowknife differ diff --git a/libs/pytz/zoneinfo/Antarctica/Casey b/libs/pytz/zoneinfo/Antarctica/Casey new file mode 100644 index 00000000..f100f474 Binary files /dev/null and b/libs/pytz/zoneinfo/Antarctica/Casey differ diff --git a/libs/pytz/zoneinfo/Antarctica/Davis b/libs/pytz/zoneinfo/Antarctica/Davis new file mode 100644 index 00000000..916f2c25 Binary files /dev/null and b/libs/pytz/zoneinfo/Antarctica/Davis differ diff --git a/libs/pytz/zoneinfo/Antarctica/DumontDUrville b/libs/pytz/zoneinfo/Antarctica/DumontDUrville new file mode 100644 index 00000000..bd6563ec Binary files /dev/null and b/libs/pytz/zoneinfo/Antarctica/DumontDUrville differ diff --git a/libs/pytz/zoneinfo/Antarctica/Macquarie b/libs/pytz/zoneinfo/Antarctica/Macquarie new file mode 100644 index 00000000..83c308ad Binary files /dev/null and b/libs/pytz/zoneinfo/Antarctica/Macquarie differ diff --git a/libs/pytz/zoneinfo/Antarctica/Mawson b/libs/pytz/zoneinfo/Antarctica/Mawson new file mode 100644 index 00000000..e1f0b09b Binary files /dev/null and b/libs/pytz/zoneinfo/Antarctica/Mawson differ diff --git a/libs/pytz/zoneinfo/Antarctica/McMurdo b/libs/pytz/zoneinfo/Antarctica/McMurdo new file mode 100644 index 00000000..60bcef68 Binary files /dev/null and b/libs/pytz/zoneinfo/Antarctica/McMurdo differ diff --git a/libs/pytz/zoneinfo/Antarctica/Palmer b/libs/pytz/zoneinfo/Antarctica/Palmer new file mode 100644 index 00000000..3dd85f84 Binary files /dev/null and b/libs/pytz/zoneinfo/Antarctica/Palmer differ diff --git a/libs/pytz/zoneinfo/Antarctica/Rothera b/libs/pytz/zoneinfo/Antarctica/Rothera new file mode 100644 index 00000000..7940e6ef Binary files /dev/null and b/libs/pytz/zoneinfo/Antarctica/Rothera differ diff --git a/libs/pytz/zoneinfo/Antarctica/South_Pole b/libs/pytz/zoneinfo/Antarctica/South_Pole new file mode 100644 index 00000000..60bcef68 Binary files /dev/null and b/libs/pytz/zoneinfo/Antarctica/South_Pole differ diff --git a/libs/pytz/zoneinfo/Antarctica/Syowa b/libs/pytz/zoneinfo/Antarctica/Syowa new file mode 100644 index 00000000..4bb041a2 Binary files /dev/null and b/libs/pytz/zoneinfo/Antarctica/Syowa differ diff --git a/libs/pytz/zoneinfo/Antarctica/Troll b/libs/pytz/zoneinfo/Antarctica/Troll new file mode 100644 index 00000000..5e565da2 Binary files /dev/null and b/libs/pytz/zoneinfo/Antarctica/Troll differ diff --git a/libs/pytz/zoneinfo/Antarctica/Vostok b/libs/pytz/zoneinfo/Antarctica/Vostok new file mode 100644 index 00000000..5696abf5 Binary files /dev/null and b/libs/pytz/zoneinfo/Antarctica/Vostok differ diff --git a/libs/pytz/zoneinfo/Arctic/Longyearbyen b/libs/pytz/zoneinfo/Arctic/Longyearbyen new file mode 100644 index 00000000..c6842af8 Binary files /dev/null and b/libs/pytz/zoneinfo/Arctic/Longyearbyen differ diff --git a/libs/pytz/zoneinfo/Asia/Aden b/libs/pytz/zoneinfo/Asia/Aden new file mode 100644 index 00000000..b2f9a255 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Aden differ diff --git a/libs/pytz/zoneinfo/Asia/Almaty b/libs/pytz/zoneinfo/Asia/Almaty new file mode 100644 index 00000000..d93201cf Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Almaty differ diff --git a/libs/pytz/zoneinfo/Asia/Amman b/libs/pytz/zoneinfo/Asia/Amman new file mode 100644 index 00000000..281b304e Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Amman differ diff --git a/libs/pytz/zoneinfo/Asia/Anadyr b/libs/pytz/zoneinfo/Asia/Anadyr new file mode 100644 index 00000000..6a966013 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Anadyr differ diff --git a/libs/pytz/zoneinfo/Asia/Aqtau b/libs/pytz/zoneinfo/Asia/Aqtau new file mode 100644 index 00000000..78cbcf0e Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Aqtau differ diff --git a/libs/pytz/zoneinfo/Asia/Aqtobe b/libs/pytz/zoneinfo/Asia/Aqtobe new file mode 100644 index 00000000..7504052a Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Aqtobe differ diff --git a/libs/pytz/zoneinfo/Asia/Ashgabat b/libs/pytz/zoneinfo/Asia/Ashgabat new file mode 100644 index 00000000..8d9e03c1 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Ashgabat differ diff --git a/libs/pytz/zoneinfo/Asia/Ashkhabad b/libs/pytz/zoneinfo/Asia/Ashkhabad new file mode 100644 index 00000000..8d9e03c1 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Ashkhabad differ diff --git a/libs/pytz/zoneinfo/Asia/Atyrau b/libs/pytz/zoneinfo/Asia/Atyrau new file mode 100644 index 00000000..317466d1 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Atyrau differ diff --git a/libs/pytz/zoneinfo/Asia/Baghdad b/libs/pytz/zoneinfo/Asia/Baghdad new file mode 100644 index 00000000..97fa6c73 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Baghdad differ diff --git a/libs/pytz/zoneinfo/Asia/Bahrain b/libs/pytz/zoneinfo/Asia/Bahrain new file mode 100644 index 00000000..f5140926 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Bahrain differ diff --git a/libs/pytz/zoneinfo/Asia/Baku b/libs/pytz/zoneinfo/Asia/Baku new file mode 100644 index 00000000..8a090d77 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Baku differ diff --git a/libs/pytz/zoneinfo/Asia/Bangkok b/libs/pytz/zoneinfo/Asia/Bangkok new file mode 100644 index 00000000..72496402 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Bangkok differ diff --git a/libs/pytz/zoneinfo/Asia/Barnaul b/libs/pytz/zoneinfo/Asia/Barnaul new file mode 100644 index 00000000..82cc49c4 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Barnaul differ diff --git a/libs/pytz/zoneinfo/Asia/Beirut b/libs/pytz/zoneinfo/Asia/Beirut new file mode 100644 index 00000000..efb24c27 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Beirut differ diff --git a/libs/pytz/zoneinfo/Asia/Bishkek b/libs/pytz/zoneinfo/Asia/Bishkek new file mode 100644 index 00000000..f7a7d548 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Bishkek differ diff --git a/libs/pytz/zoneinfo/Asia/Brunei b/libs/pytz/zoneinfo/Asia/Brunei new file mode 100644 index 00000000..8624c7ae Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Brunei differ diff --git a/libs/pytz/zoneinfo/Asia/Calcutta b/libs/pytz/zoneinfo/Asia/Calcutta new file mode 100644 index 00000000..e1cfcb8d Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Calcutta differ diff --git a/libs/pytz/zoneinfo/Asia/Chita b/libs/pytz/zoneinfo/Asia/Chita new file mode 100644 index 00000000..3baf7528 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Chita differ diff --git a/libs/pytz/zoneinfo/Asia/Choibalsan b/libs/pytz/zoneinfo/Asia/Choibalsan new file mode 100644 index 00000000..79b9d3c8 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Choibalsan differ diff --git a/libs/pytz/zoneinfo/Asia/Chongqing b/libs/pytz/zoneinfo/Asia/Chongqing new file mode 100644 index 00000000..ce9e00a5 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Chongqing differ diff --git a/libs/pytz/zoneinfo/Asia/Chungking b/libs/pytz/zoneinfo/Asia/Chungking new file mode 100644 index 00000000..ce9e00a5 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Chungking differ diff --git a/libs/pytz/zoneinfo/Asia/Colombo b/libs/pytz/zoneinfo/Asia/Colombo new file mode 100644 index 00000000..4fc96c89 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Colombo differ diff --git a/libs/pytz/zoneinfo/Asia/Dacca b/libs/pytz/zoneinfo/Asia/Dacca new file mode 100644 index 00000000..776f27da Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Dacca differ diff --git a/libs/pytz/zoneinfo/Asia/Damascus b/libs/pytz/zoneinfo/Asia/Damascus new file mode 100644 index 00000000..4b610b5a Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Damascus differ diff --git a/libs/pytz/zoneinfo/Asia/Dhaka b/libs/pytz/zoneinfo/Asia/Dhaka new file mode 100644 index 00000000..776f27da Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Dhaka differ diff --git a/libs/pytz/zoneinfo/Asia/Dili b/libs/pytz/zoneinfo/Asia/Dili new file mode 100644 index 00000000..f6ce91a1 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Dili differ diff --git a/libs/pytz/zoneinfo/Asia/Dubai b/libs/pytz/zoneinfo/Asia/Dubai new file mode 100644 index 00000000..7880d5d7 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Dubai differ diff --git a/libs/pytz/zoneinfo/Asia/Dushanbe b/libs/pytz/zoneinfo/Asia/Dushanbe new file mode 100644 index 00000000..694f6e6a Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Dushanbe differ diff --git a/libs/pytz/zoneinfo/Asia/Famagusta b/libs/pytz/zoneinfo/Asia/Famagusta new file mode 100644 index 00000000..653b146a Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Famagusta differ diff --git a/libs/pytz/zoneinfo/Asia/Gaza b/libs/pytz/zoneinfo/Asia/Gaza new file mode 100644 index 00000000..cf54deb8 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Gaza differ diff --git a/libs/pytz/zoneinfo/Asia/Harbin b/libs/pytz/zoneinfo/Asia/Harbin new file mode 100644 index 00000000..ce9e00a5 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Harbin differ diff --git a/libs/pytz/zoneinfo/Asia/Hebron b/libs/pytz/zoneinfo/Asia/Hebron new file mode 100644 index 00000000..09c876a6 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Hebron differ diff --git a/libs/pytz/zoneinfo/Asia/Ho_Chi_Minh b/libs/pytz/zoneinfo/Asia/Ho_Chi_Minh new file mode 100644 index 00000000..eab94fe8 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Ho_Chi_Minh differ diff --git a/libs/pytz/zoneinfo/Asia/Hong_Kong b/libs/pytz/zoneinfo/Asia/Hong_Kong new file mode 100644 index 00000000..8e5c5813 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Hong_Kong differ diff --git a/libs/pytz/zoneinfo/Asia/Hovd b/libs/pytz/zoneinfo/Asia/Hovd new file mode 100644 index 00000000..8eb5f647 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Hovd differ diff --git a/libs/pytz/zoneinfo/Asia/Irkutsk b/libs/pytz/zoneinfo/Asia/Irkutsk new file mode 100644 index 00000000..e8c53c0d Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Irkutsk differ diff --git a/libs/pytz/zoneinfo/Asia/Istanbul b/libs/pytz/zoneinfo/Asia/Istanbul new file mode 100644 index 00000000..833d4eba Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Istanbul differ diff --git a/libs/pytz/zoneinfo/Asia/Jakarta b/libs/pytz/zoneinfo/Asia/Jakarta new file mode 100644 index 00000000..673d4801 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Jakarta differ diff --git a/libs/pytz/zoneinfo/Asia/Jayapura b/libs/pytz/zoneinfo/Asia/Jayapura new file mode 100644 index 00000000..a4c08297 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Jayapura differ diff --git a/libs/pytz/zoneinfo/Asia/Jerusalem b/libs/pytz/zoneinfo/Asia/Jerusalem new file mode 100644 index 00000000..2d14c999 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Jerusalem differ diff --git a/libs/pytz/zoneinfo/Asia/Kabul b/libs/pytz/zoneinfo/Asia/Kabul new file mode 100644 index 00000000..a22cf592 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Kabul differ diff --git a/libs/pytz/zoneinfo/Asia/Kamchatka b/libs/pytz/zoneinfo/Asia/Kamchatka new file mode 100644 index 00000000..b9ed49ca Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Kamchatka differ diff --git a/libs/pytz/zoneinfo/Asia/Karachi b/libs/pytz/zoneinfo/Asia/Karachi new file mode 100644 index 00000000..337e1d58 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Karachi differ diff --git a/libs/pytz/zoneinfo/Asia/Kashgar b/libs/pytz/zoneinfo/Asia/Kashgar new file mode 100644 index 00000000..0342b433 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Kashgar differ diff --git a/libs/pytz/zoneinfo/Asia/Kathmandu b/libs/pytz/zoneinfo/Asia/Kathmandu new file mode 100644 index 00000000..2f810b12 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Kathmandu differ diff --git a/libs/pytz/zoneinfo/Asia/Katmandu b/libs/pytz/zoneinfo/Asia/Katmandu new file mode 100644 index 00000000..2f810b12 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Katmandu differ diff --git a/libs/pytz/zoneinfo/Asia/Khandyga b/libs/pytz/zoneinfo/Asia/Khandyga new file mode 100644 index 00000000..2b2f5bfa Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Khandyga differ diff --git a/libs/pytz/zoneinfo/Asia/Kolkata b/libs/pytz/zoneinfo/Asia/Kolkata new file mode 100644 index 00000000..e1cfcb8d Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Kolkata differ diff --git a/libs/pytz/zoneinfo/Asia/Krasnoyarsk b/libs/pytz/zoneinfo/Asia/Krasnoyarsk new file mode 100644 index 00000000..59efd24c Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Krasnoyarsk differ diff --git a/libs/pytz/zoneinfo/Asia/Kuala_Lumpur b/libs/pytz/zoneinfo/Asia/Kuala_Lumpur new file mode 100644 index 00000000..6d7d47b9 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Kuala_Lumpur differ diff --git a/libs/pytz/zoneinfo/Asia/Kuching b/libs/pytz/zoneinfo/Asia/Kuching new file mode 100644 index 00000000..4878622d Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Kuching differ diff --git a/libs/pytz/zoneinfo/Asia/Kuwait b/libs/pytz/zoneinfo/Asia/Kuwait new file mode 100644 index 00000000..b2f9a255 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Kuwait differ diff --git a/libs/pytz/zoneinfo/Asia/Macao b/libs/pytz/zoneinfo/Asia/Macao new file mode 100644 index 00000000..d801000d Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Macao differ diff --git a/libs/pytz/zoneinfo/Asia/Macau b/libs/pytz/zoneinfo/Asia/Macau new file mode 100644 index 00000000..d801000d Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Macau differ diff --git a/libs/pytz/zoneinfo/Asia/Magadan b/libs/pytz/zoneinfo/Asia/Magadan new file mode 100644 index 00000000..b20cc57e Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Magadan differ diff --git a/libs/pytz/zoneinfo/Asia/Makassar b/libs/pytz/zoneinfo/Asia/Makassar new file mode 100644 index 00000000..ed55442e Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Makassar differ diff --git a/libs/pytz/zoneinfo/Asia/Manila b/libs/pytz/zoneinfo/Asia/Manila new file mode 100644 index 00000000..2c9220c9 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Manila differ diff --git a/libs/pytz/zoneinfo/Asia/Muscat b/libs/pytz/zoneinfo/Asia/Muscat new file mode 100644 index 00000000..7880d5d7 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Muscat differ diff --git a/libs/pytz/zoneinfo/Asia/Nicosia b/libs/pytz/zoneinfo/Asia/Nicosia new file mode 100644 index 00000000..f7f10ab7 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Nicosia differ diff --git a/libs/pytz/zoneinfo/Asia/Novokuznetsk b/libs/pytz/zoneinfo/Asia/Novokuznetsk new file mode 100644 index 00000000..2576a3b0 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Novokuznetsk differ diff --git a/libs/pytz/zoneinfo/Asia/Novosibirsk b/libs/pytz/zoneinfo/Asia/Novosibirsk new file mode 100644 index 00000000..95e3c73b Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Novosibirsk differ diff --git a/libs/pytz/zoneinfo/Asia/Omsk b/libs/pytz/zoneinfo/Asia/Omsk new file mode 100644 index 00000000..d805e4f7 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Omsk differ diff --git a/libs/pytz/zoneinfo/Asia/Oral b/libs/pytz/zoneinfo/Asia/Oral new file mode 100644 index 00000000..e36aec47 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Oral differ diff --git a/libs/pytz/zoneinfo/Asia/Phnom_Penh b/libs/pytz/zoneinfo/Asia/Phnom_Penh new file mode 100644 index 00000000..72496402 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Phnom_Penh differ diff --git a/libs/pytz/zoneinfo/Asia/Pontianak b/libs/pytz/zoneinfo/Asia/Pontianak new file mode 100644 index 00000000..9377d038 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Pontianak differ diff --git a/libs/pytz/zoneinfo/Asia/Pyongyang b/libs/pytz/zoneinfo/Asia/Pyongyang new file mode 100644 index 00000000..dd54989f Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Pyongyang differ diff --git a/libs/pytz/zoneinfo/Asia/Qatar b/libs/pytz/zoneinfo/Asia/Qatar new file mode 100644 index 00000000..f5140926 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Qatar differ diff --git a/libs/pytz/zoneinfo/Asia/Qyzylorda b/libs/pytz/zoneinfo/Asia/Qyzylorda new file mode 100644 index 00000000..00b27844 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Qyzylorda differ diff --git a/libs/pytz/zoneinfo/Asia/Rangoon b/libs/pytz/zoneinfo/Asia/Rangoon new file mode 100644 index 00000000..a00282de Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Rangoon differ diff --git a/libs/pytz/zoneinfo/Asia/Riyadh b/libs/pytz/zoneinfo/Asia/Riyadh new file mode 100644 index 00000000..b2f9a255 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Riyadh differ diff --git a/libs/pytz/zoneinfo/Asia/Saigon b/libs/pytz/zoneinfo/Asia/Saigon new file mode 100644 index 00000000..eab94fe8 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Saigon differ diff --git a/libs/pytz/zoneinfo/Asia/Sakhalin b/libs/pytz/zoneinfo/Asia/Sakhalin new file mode 100644 index 00000000..9c94900c Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Sakhalin differ diff --git a/libs/pytz/zoneinfo/Asia/Samarkand b/libs/pytz/zoneinfo/Asia/Samarkand new file mode 100644 index 00000000..a5d1e970 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Samarkand differ diff --git a/libs/pytz/zoneinfo/Asia/Seoul b/libs/pytz/zoneinfo/Asia/Seoul new file mode 100644 index 00000000..fa1cbd39 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Seoul differ diff --git a/libs/pytz/zoneinfo/Asia/Shanghai b/libs/pytz/zoneinfo/Asia/Shanghai new file mode 100644 index 00000000..ce9e00a5 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Shanghai differ diff --git a/libs/pytz/zoneinfo/Asia/Singapore b/libs/pytz/zoneinfo/Asia/Singapore new file mode 100644 index 00000000..ebc4b0d9 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Singapore differ diff --git a/libs/pytz/zoneinfo/Asia/Srednekolymsk b/libs/pytz/zoneinfo/Asia/Srednekolymsk new file mode 100644 index 00000000..f8b7bb21 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Srednekolymsk differ diff --git a/libs/pytz/zoneinfo/Asia/Taipei b/libs/pytz/zoneinfo/Asia/Taipei new file mode 100644 index 00000000..f9cbe672 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Taipei differ diff --git a/libs/pytz/zoneinfo/Asia/Tashkent b/libs/pytz/zoneinfo/Asia/Tashkent new file mode 100644 index 00000000..e75bb365 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Tashkent differ diff --git a/libs/pytz/zoneinfo/Asia/Tbilisi b/libs/pytz/zoneinfo/Asia/Tbilisi new file mode 100644 index 00000000..09bb06eb Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Tbilisi differ diff --git a/libs/pytz/zoneinfo/Asia/Tehran b/libs/pytz/zoneinfo/Asia/Tehran new file mode 100644 index 00000000..ad9058b4 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Tehran differ diff --git a/libs/pytz/zoneinfo/Asia/Tel_Aviv b/libs/pytz/zoneinfo/Asia/Tel_Aviv new file mode 100644 index 00000000..2d14c999 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Tel_Aviv differ diff --git a/libs/pytz/zoneinfo/Asia/Thimbu b/libs/pytz/zoneinfo/Asia/Thimbu new file mode 100644 index 00000000..06d3324d Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Thimbu differ diff --git a/libs/pytz/zoneinfo/Asia/Thimphu b/libs/pytz/zoneinfo/Asia/Thimphu new file mode 100644 index 00000000..06d3324d Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Thimphu differ diff --git a/libs/pytz/zoneinfo/Asia/Tokyo b/libs/pytz/zoneinfo/Asia/Tokyo new file mode 100644 index 00000000..26f4d34d Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Tokyo differ diff --git a/libs/pytz/zoneinfo/Asia/Tomsk b/libs/pytz/zoneinfo/Asia/Tomsk new file mode 100644 index 00000000..28da9c90 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Tomsk differ diff --git a/libs/pytz/zoneinfo/Asia/Ujung_Pandang b/libs/pytz/zoneinfo/Asia/Ujung_Pandang new file mode 100644 index 00000000..ed55442e Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Ujung_Pandang differ diff --git a/libs/pytz/zoneinfo/Asia/Ulaanbaatar b/libs/pytz/zoneinfo/Asia/Ulaanbaatar new file mode 100644 index 00000000..82fd4760 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Ulaanbaatar differ diff --git a/libs/pytz/zoneinfo/Asia/Ulan_Bator b/libs/pytz/zoneinfo/Asia/Ulan_Bator new file mode 100644 index 00000000..82fd4760 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Ulan_Bator differ diff --git a/libs/pytz/zoneinfo/Asia/Urumqi b/libs/pytz/zoneinfo/Asia/Urumqi new file mode 100644 index 00000000..0342b433 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Urumqi differ diff --git a/libs/pytz/zoneinfo/Asia/Ust-Nera b/libs/pytz/zoneinfo/Asia/Ust-Nera new file mode 100644 index 00000000..c0c3767e Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Ust-Nera differ diff --git a/libs/pytz/zoneinfo/Asia/Vientiane b/libs/pytz/zoneinfo/Asia/Vientiane new file mode 100644 index 00000000..72496402 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Vientiane differ diff --git a/libs/pytz/zoneinfo/Asia/Vladivostok b/libs/pytz/zoneinfo/Asia/Vladivostok new file mode 100644 index 00000000..15731abc Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Vladivostok differ diff --git a/libs/pytz/zoneinfo/Asia/Yakutsk b/libs/pytz/zoneinfo/Asia/Yakutsk new file mode 100644 index 00000000..1f86e77f Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Yakutsk differ diff --git a/libs/pytz/zoneinfo/Asia/Yangon b/libs/pytz/zoneinfo/Asia/Yangon new file mode 100644 index 00000000..a00282de Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Yangon differ diff --git a/libs/pytz/zoneinfo/Asia/Yekaterinburg b/libs/pytz/zoneinfo/Asia/Yekaterinburg new file mode 100644 index 00000000..fff9f3b1 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Yekaterinburg differ diff --git a/libs/pytz/zoneinfo/Asia/Yerevan b/libs/pytz/zoneinfo/Asia/Yerevan new file mode 100644 index 00000000..409c3b17 Binary files /dev/null and b/libs/pytz/zoneinfo/Asia/Yerevan differ diff --git a/libs/pytz/zoneinfo/Atlantic/Azores b/libs/pytz/zoneinfo/Atlantic/Azores new file mode 100644 index 00000000..56593dbf Binary files /dev/null and b/libs/pytz/zoneinfo/Atlantic/Azores differ diff --git a/libs/pytz/zoneinfo/Atlantic/Bermuda b/libs/pytz/zoneinfo/Atlantic/Bermuda new file mode 100644 index 00000000..3a5c6dbf Binary files /dev/null and b/libs/pytz/zoneinfo/Atlantic/Bermuda differ diff --git a/libs/pytz/zoneinfo/Atlantic/Canary b/libs/pytz/zoneinfo/Atlantic/Canary new file mode 100644 index 00000000..f3192156 Binary files /dev/null and b/libs/pytz/zoneinfo/Atlantic/Canary differ diff --git a/libs/pytz/zoneinfo/Atlantic/Cape_Verde b/libs/pytz/zoneinfo/Atlantic/Cape_Verde new file mode 100644 index 00000000..e2a49d24 Binary files /dev/null and b/libs/pytz/zoneinfo/Atlantic/Cape_Verde differ diff --git a/libs/pytz/zoneinfo/Atlantic/Faeroe b/libs/pytz/zoneinfo/Atlantic/Faeroe new file mode 100644 index 00000000..4dab7ef0 Binary files /dev/null and b/libs/pytz/zoneinfo/Atlantic/Faeroe differ diff --git a/libs/pytz/zoneinfo/Atlantic/Faroe b/libs/pytz/zoneinfo/Atlantic/Faroe new file mode 100644 index 00000000..4dab7ef0 Binary files /dev/null and b/libs/pytz/zoneinfo/Atlantic/Faroe differ diff --git a/libs/pytz/zoneinfo/Atlantic/Jan_Mayen b/libs/pytz/zoneinfo/Atlantic/Jan_Mayen new file mode 100644 index 00000000..c6842af8 Binary files /dev/null and b/libs/pytz/zoneinfo/Atlantic/Jan_Mayen differ diff --git a/libs/pytz/zoneinfo/Atlantic/Madeira b/libs/pytz/zoneinfo/Atlantic/Madeira new file mode 100644 index 00000000..5213761f Binary files /dev/null and b/libs/pytz/zoneinfo/Atlantic/Madeira differ diff --git a/libs/pytz/zoneinfo/Atlantic/Reykjavik b/libs/pytz/zoneinfo/Atlantic/Reykjavik new file mode 100644 index 00000000..ac6bd697 Binary files /dev/null and b/libs/pytz/zoneinfo/Atlantic/Reykjavik differ diff --git a/libs/pytz/zoneinfo/Atlantic/South_Georgia b/libs/pytz/zoneinfo/Atlantic/South_Georgia new file mode 100644 index 00000000..b3311b63 Binary files /dev/null and b/libs/pytz/zoneinfo/Atlantic/South_Georgia differ diff --git a/libs/pytz/zoneinfo/Atlantic/St_Helena b/libs/pytz/zoneinfo/Atlantic/St_Helena new file mode 100644 index 00000000..65d19ec2 Binary files /dev/null and b/libs/pytz/zoneinfo/Atlantic/St_Helena differ diff --git a/libs/pytz/zoneinfo/Atlantic/Stanley b/libs/pytz/zoneinfo/Atlantic/Stanley new file mode 100644 index 00000000..2fd42a2c Binary files /dev/null and b/libs/pytz/zoneinfo/Atlantic/Stanley differ diff --git a/libs/pytz/zoneinfo/Australia/ACT b/libs/pytz/zoneinfo/Australia/ACT new file mode 100644 index 00000000..4ed4467f Binary files /dev/null and b/libs/pytz/zoneinfo/Australia/ACT differ diff --git a/libs/pytz/zoneinfo/Australia/Adelaide b/libs/pytz/zoneinfo/Australia/Adelaide new file mode 100644 index 00000000..190b0e33 Binary files /dev/null and b/libs/pytz/zoneinfo/Australia/Adelaide differ diff --git a/libs/pytz/zoneinfo/Australia/Brisbane b/libs/pytz/zoneinfo/Australia/Brisbane new file mode 100644 index 00000000..26ffd9ac Binary files /dev/null and b/libs/pytz/zoneinfo/Australia/Brisbane differ diff --git a/libs/pytz/zoneinfo/Australia/Broken_Hill b/libs/pytz/zoneinfo/Australia/Broken_Hill new file mode 100644 index 00000000..874c8650 Binary files /dev/null and b/libs/pytz/zoneinfo/Australia/Broken_Hill differ diff --git a/libs/pytz/zoneinfo/Australia/Canberra b/libs/pytz/zoneinfo/Australia/Canberra new file mode 100644 index 00000000..4ed4467f Binary files /dev/null and b/libs/pytz/zoneinfo/Australia/Canberra differ diff --git a/libs/pytz/zoneinfo/Australia/Currie b/libs/pytz/zoneinfo/Australia/Currie new file mode 100644 index 00000000..865801e5 Binary files /dev/null and b/libs/pytz/zoneinfo/Australia/Currie differ diff --git a/libs/pytz/zoneinfo/Australia/Darwin b/libs/pytz/zoneinfo/Australia/Darwin new file mode 100644 index 00000000..cf42d1d8 Binary files /dev/null and b/libs/pytz/zoneinfo/Australia/Darwin differ diff --git a/libs/pytz/zoneinfo/Australia/Eucla b/libs/pytz/zoneinfo/Australia/Eucla new file mode 100644 index 00000000..c49d499c Binary files /dev/null and b/libs/pytz/zoneinfo/Australia/Eucla differ diff --git a/libs/pytz/zoneinfo/Australia/Hobart b/libs/pytz/zoneinfo/Australia/Hobart new file mode 100644 index 00000000..92d1215d Binary files /dev/null and b/libs/pytz/zoneinfo/Australia/Hobart differ diff --git a/libs/pytz/zoneinfo/Australia/LHI b/libs/pytz/zoneinfo/Australia/LHI new file mode 100644 index 00000000..8c6c7dd0 Binary files /dev/null and b/libs/pytz/zoneinfo/Australia/LHI differ diff --git a/libs/pytz/zoneinfo/Australia/Lindeman b/libs/pytz/zoneinfo/Australia/Lindeman new file mode 100644 index 00000000..8ee1a6f5 Binary files /dev/null and b/libs/pytz/zoneinfo/Australia/Lindeman differ diff --git a/libs/pytz/zoneinfo/Australia/Lord_Howe b/libs/pytz/zoneinfo/Australia/Lord_Howe new file mode 100644 index 00000000..8c6c7dd0 Binary files /dev/null and b/libs/pytz/zoneinfo/Australia/Lord_Howe differ diff --git a/libs/pytz/zoneinfo/Australia/Melbourne b/libs/pytz/zoneinfo/Australia/Melbourne new file mode 100644 index 00000000..3f2d3d7f Binary files /dev/null and b/libs/pytz/zoneinfo/Australia/Melbourne differ diff --git a/libs/pytz/zoneinfo/Australia/NSW b/libs/pytz/zoneinfo/Australia/NSW new file mode 100644 index 00000000..4ed4467f Binary files /dev/null and b/libs/pytz/zoneinfo/Australia/NSW differ diff --git a/libs/pytz/zoneinfo/Australia/North b/libs/pytz/zoneinfo/Australia/North new file mode 100644 index 00000000..cf42d1d8 Binary files /dev/null and b/libs/pytz/zoneinfo/Australia/North differ diff --git a/libs/pytz/zoneinfo/Australia/Perth b/libs/pytz/zoneinfo/Australia/Perth new file mode 100644 index 00000000..d38b67e2 Binary files /dev/null and b/libs/pytz/zoneinfo/Australia/Perth differ diff --git a/libs/pytz/zoneinfo/Australia/Queensland b/libs/pytz/zoneinfo/Australia/Queensland new file mode 100644 index 00000000..26ffd9ac Binary files /dev/null and b/libs/pytz/zoneinfo/Australia/Queensland differ diff --git a/libs/pytz/zoneinfo/Australia/South b/libs/pytz/zoneinfo/Australia/South new file mode 100644 index 00000000..190b0e33 Binary files /dev/null and b/libs/pytz/zoneinfo/Australia/South differ diff --git a/libs/pytz/zoneinfo/Australia/Sydney b/libs/pytz/zoneinfo/Australia/Sydney new file mode 100644 index 00000000..4ed4467f Binary files /dev/null and b/libs/pytz/zoneinfo/Australia/Sydney differ diff --git a/libs/pytz/zoneinfo/Australia/Tasmania b/libs/pytz/zoneinfo/Australia/Tasmania new file mode 100644 index 00000000..92d1215d Binary files /dev/null and b/libs/pytz/zoneinfo/Australia/Tasmania differ diff --git a/libs/pytz/zoneinfo/Australia/Victoria b/libs/pytz/zoneinfo/Australia/Victoria new file mode 100644 index 00000000..3f2d3d7f Binary files /dev/null and b/libs/pytz/zoneinfo/Australia/Victoria differ diff --git a/libs/pytz/zoneinfo/Australia/West b/libs/pytz/zoneinfo/Australia/West new file mode 100644 index 00000000..d38b67e2 Binary files /dev/null and b/libs/pytz/zoneinfo/Australia/West differ diff --git a/libs/pytz/zoneinfo/Australia/Yancowinna b/libs/pytz/zoneinfo/Australia/Yancowinna new file mode 100644 index 00000000..874c8650 Binary files /dev/null and b/libs/pytz/zoneinfo/Australia/Yancowinna differ diff --git a/libs/pytz/zoneinfo/Brazil/Acre b/libs/pytz/zoneinfo/Brazil/Acre new file mode 100644 index 00000000..16b7f923 Binary files /dev/null and b/libs/pytz/zoneinfo/Brazil/Acre differ diff --git a/libs/pytz/zoneinfo/Brazil/DeNoronha b/libs/pytz/zoneinfo/Brazil/DeNoronha new file mode 100644 index 00000000..95ff8a25 Binary files /dev/null and b/libs/pytz/zoneinfo/Brazil/DeNoronha differ diff --git a/libs/pytz/zoneinfo/Brazil/East b/libs/pytz/zoneinfo/Brazil/East new file mode 100644 index 00000000..c417ba1d Binary files /dev/null and b/libs/pytz/zoneinfo/Brazil/East differ diff --git a/libs/pytz/zoneinfo/Brazil/West b/libs/pytz/zoneinfo/Brazil/West new file mode 100644 index 00000000..b10241e6 Binary files /dev/null and b/libs/pytz/zoneinfo/Brazil/West differ diff --git a/libs/pytz/zoneinfo/CET b/libs/pytz/zoneinfo/CET new file mode 100644 index 00000000..d585656f Binary files /dev/null and b/libs/pytz/zoneinfo/CET differ diff --git a/libs/pytz/zoneinfo/CST6CDT b/libs/pytz/zoneinfo/CST6CDT new file mode 100644 index 00000000..41c4136f Binary files /dev/null and b/libs/pytz/zoneinfo/CST6CDT differ diff --git a/libs/pytz/zoneinfo/Canada/Atlantic b/libs/pytz/zoneinfo/Canada/Atlantic new file mode 100644 index 00000000..756099ab Binary files /dev/null and b/libs/pytz/zoneinfo/Canada/Atlantic differ diff --git a/libs/pytz/zoneinfo/Canada/Central b/libs/pytz/zoneinfo/Canada/Central new file mode 100644 index 00000000..3718d47d Binary files /dev/null and b/libs/pytz/zoneinfo/Canada/Central differ diff --git a/libs/pytz/zoneinfo/Canada/Eastern b/libs/pytz/zoneinfo/Canada/Eastern new file mode 100644 index 00000000..6752c5b0 Binary files /dev/null and b/libs/pytz/zoneinfo/Canada/Eastern differ diff --git a/libs/pytz/zoneinfo/Canada/Mountain b/libs/pytz/zoneinfo/Canada/Mountain new file mode 100644 index 00000000..3fa05798 Binary files /dev/null and b/libs/pytz/zoneinfo/Canada/Mountain differ diff --git a/libs/pytz/zoneinfo/Canada/Newfoundland b/libs/pytz/zoneinfo/Canada/Newfoundland new file mode 100644 index 00000000..65a5b0c7 Binary files /dev/null and b/libs/pytz/zoneinfo/Canada/Newfoundland differ diff --git a/libs/pytz/zoneinfo/Canada/Pacific b/libs/pytz/zoneinfo/Canada/Pacific new file mode 100644 index 00000000..0f9f8328 Binary files /dev/null and b/libs/pytz/zoneinfo/Canada/Pacific differ diff --git a/libs/pytz/zoneinfo/Canada/Saskatchewan b/libs/pytz/zoneinfo/Canada/Saskatchewan new file mode 100644 index 00000000..20c9c84d Binary files /dev/null and b/libs/pytz/zoneinfo/Canada/Saskatchewan differ diff --git a/libs/pytz/zoneinfo/Canada/Yukon b/libs/pytz/zoneinfo/Canada/Yukon new file mode 100644 index 00000000..fb3cd71a Binary files /dev/null and b/libs/pytz/zoneinfo/Canada/Yukon differ diff --git a/libs/pytz/zoneinfo/Chile/Continental b/libs/pytz/zoneinfo/Chile/Continental new file mode 100644 index 00000000..816a0428 Binary files /dev/null and b/libs/pytz/zoneinfo/Chile/Continental differ diff --git a/libs/pytz/zoneinfo/Chile/EasterIsland b/libs/pytz/zoneinfo/Chile/EasterIsland new file mode 100644 index 00000000..cae37440 Binary files /dev/null and b/libs/pytz/zoneinfo/Chile/EasterIsland differ diff --git a/libs/pytz/zoneinfo/Cuba b/libs/pytz/zoneinfo/Cuba new file mode 100644 index 00000000..8186060a Binary files /dev/null and b/libs/pytz/zoneinfo/Cuba differ diff --git a/libs/pytz/zoneinfo/EET b/libs/pytz/zoneinfo/EET new file mode 100644 index 00000000..d2f54c9b Binary files /dev/null and b/libs/pytz/zoneinfo/EET differ diff --git a/libs/pytz/zoneinfo/EST b/libs/pytz/zoneinfo/EST new file mode 100644 index 00000000..074a4fc7 Binary files /dev/null and b/libs/pytz/zoneinfo/EST differ diff --git a/libs/pytz/zoneinfo/EST5EDT b/libs/pytz/zoneinfo/EST5EDT new file mode 100644 index 00000000..087b641d Binary files /dev/null and b/libs/pytz/zoneinfo/EST5EDT differ diff --git a/libs/pytz/zoneinfo/Egypt b/libs/pytz/zoneinfo/Egypt new file mode 100644 index 00000000..0272fa1b Binary files /dev/null and b/libs/pytz/zoneinfo/Egypt differ diff --git a/libs/pytz/zoneinfo/Eire b/libs/pytz/zoneinfo/Eire new file mode 100644 index 00000000..5c5a7a3b Binary files /dev/null and b/libs/pytz/zoneinfo/Eire differ diff --git a/libs/pytz/zoneinfo/Etc/GMT b/libs/pytz/zoneinfo/Etc/GMT new file mode 100644 index 00000000..2ee14295 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT differ diff --git a/libs/pytz/zoneinfo/Etc/GMT+0 b/libs/pytz/zoneinfo/Etc/GMT+0 new file mode 100644 index 00000000..2ee14295 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT+0 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT+1 b/libs/pytz/zoneinfo/Etc/GMT+1 new file mode 100644 index 00000000..087d1f92 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT+1 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT+10 b/libs/pytz/zoneinfo/Etc/GMT+10 new file mode 100644 index 00000000..6437c684 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT+10 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT+11 b/libs/pytz/zoneinfo/Etc/GMT+11 new file mode 100644 index 00000000..72a912e0 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT+11 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT+12 b/libs/pytz/zoneinfo/Etc/GMT+12 new file mode 100644 index 00000000..6938a1af Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT+12 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT+2 b/libs/pytz/zoneinfo/Etc/GMT+2 new file mode 100644 index 00000000..a3155777 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT+2 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT+3 b/libs/pytz/zoneinfo/Etc/GMT+3 new file mode 100644 index 00000000..ee776199 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT+3 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT+4 b/libs/pytz/zoneinfo/Etc/GMT+4 new file mode 100644 index 00000000..1ea7da29 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT+4 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT+5 b/libs/pytz/zoneinfo/Etc/GMT+5 new file mode 100644 index 00000000..dda1a9e1 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT+5 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT+6 b/libs/pytz/zoneinfo/Etc/GMT+6 new file mode 100644 index 00000000..f4a03855 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT+6 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT+7 b/libs/pytz/zoneinfo/Etc/GMT+7 new file mode 100644 index 00000000..2d2ccd00 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT+7 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT+8 b/libs/pytz/zoneinfo/Etc/GMT+8 new file mode 100644 index 00000000..826c7700 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT+8 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT+9 b/libs/pytz/zoneinfo/Etc/GMT+9 new file mode 100644 index 00000000..b125ad2b Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT+9 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT-0 b/libs/pytz/zoneinfo/Etc/GMT-0 new file mode 100644 index 00000000..2ee14295 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT-0 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT-1 b/libs/pytz/zoneinfo/Etc/GMT-1 new file mode 100644 index 00000000..dde682d8 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT-1 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT-10 b/libs/pytz/zoneinfo/Etc/GMT-10 new file mode 100644 index 00000000..352ec08a Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT-10 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT-11 b/libs/pytz/zoneinfo/Etc/GMT-11 new file mode 100644 index 00000000..dfa27fec Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT-11 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT-12 b/libs/pytz/zoneinfo/Etc/GMT-12 new file mode 100644 index 00000000..eef949df Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT-12 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT-13 b/libs/pytz/zoneinfo/Etc/GMT-13 new file mode 100644 index 00000000..f9363b24 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT-13 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT-14 b/libs/pytz/zoneinfo/Etc/GMT-14 new file mode 100644 index 00000000..35add05a Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT-14 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT-2 b/libs/pytz/zoneinfo/Etc/GMT-2 new file mode 100644 index 00000000..315cae4f Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT-2 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT-3 b/libs/pytz/zoneinfo/Etc/GMT-3 new file mode 100644 index 00000000..7489a153 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT-3 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT-4 b/libs/pytz/zoneinfo/Etc/GMT-4 new file mode 100644 index 00000000..560243e8 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT-4 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT-5 b/libs/pytz/zoneinfo/Etc/GMT-5 new file mode 100644 index 00000000..b2bbe977 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT-5 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT-6 b/libs/pytz/zoneinfo/Etc/GMT-6 new file mode 100644 index 00000000..b979dbbc Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT-6 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT-7 b/libs/pytz/zoneinfo/Etc/GMT-7 new file mode 100644 index 00000000..365ab1f6 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT-7 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT-8 b/libs/pytz/zoneinfo/Etc/GMT-8 new file mode 100644 index 00000000..742082fc Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT-8 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT-9 b/libs/pytz/zoneinfo/Etc/GMT-9 new file mode 100644 index 00000000..abc0b275 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT-9 differ diff --git a/libs/pytz/zoneinfo/Etc/GMT0 b/libs/pytz/zoneinfo/Etc/GMT0 new file mode 100644 index 00000000..2ee14295 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/GMT0 differ diff --git a/libs/pytz/zoneinfo/Etc/Greenwich b/libs/pytz/zoneinfo/Etc/Greenwich new file mode 100644 index 00000000..2ee14295 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/Greenwich differ diff --git a/libs/pytz/zoneinfo/Etc/UCT b/libs/pytz/zoneinfo/Etc/UCT new file mode 100644 index 00000000..a88c4b66 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/UCT differ diff --git a/libs/pytz/zoneinfo/Etc/UTC b/libs/pytz/zoneinfo/Etc/UTC new file mode 100644 index 00000000..5583f5b0 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/UTC differ diff --git a/libs/pytz/zoneinfo/Etc/Universal b/libs/pytz/zoneinfo/Etc/Universal new file mode 100644 index 00000000..5583f5b0 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/Universal differ diff --git a/libs/pytz/zoneinfo/Etc/Zulu b/libs/pytz/zoneinfo/Etc/Zulu new file mode 100644 index 00000000..5583f5b0 Binary files /dev/null and b/libs/pytz/zoneinfo/Etc/Zulu differ diff --git a/libs/pytz/zoneinfo/Europe/Amsterdam b/libs/pytz/zoneinfo/Europe/Amsterdam new file mode 100644 index 00000000..ed064ed4 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Amsterdam differ diff --git a/libs/pytz/zoneinfo/Europe/Andorra b/libs/pytz/zoneinfo/Europe/Andorra new file mode 100644 index 00000000..59625503 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Andorra differ diff --git a/libs/pytz/zoneinfo/Europe/Astrakhan b/libs/pytz/zoneinfo/Europe/Astrakhan new file mode 100644 index 00000000..5e069ea5 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Astrakhan differ diff --git a/libs/pytz/zoneinfo/Europe/Athens b/libs/pytz/zoneinfo/Europe/Athens new file mode 100644 index 00000000..9f3a0678 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Athens differ diff --git a/libs/pytz/zoneinfo/Europe/Belfast b/libs/pytz/zoneinfo/Europe/Belfast new file mode 100644 index 00000000..a340326e Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Belfast differ diff --git a/libs/pytz/zoneinfo/Europe/Belgrade b/libs/pytz/zoneinfo/Europe/Belgrade new file mode 100644 index 00000000..32a57223 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Belgrade differ diff --git a/libs/pytz/zoneinfo/Europe/Berlin b/libs/pytz/zoneinfo/Europe/Berlin new file mode 100644 index 00000000..7ddd510e Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Berlin differ diff --git a/libs/pytz/zoneinfo/Europe/Bratislava b/libs/pytz/zoneinfo/Europe/Bratislava new file mode 100644 index 00000000..85036de3 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Bratislava differ diff --git a/libs/pytz/zoneinfo/Europe/Brussels b/libs/pytz/zoneinfo/Europe/Brussels new file mode 100644 index 00000000..d0d0a08a Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Brussels differ diff --git a/libs/pytz/zoneinfo/Europe/Bucharest b/libs/pytz/zoneinfo/Europe/Bucharest new file mode 100644 index 00000000..4eb7ed0d Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Bucharest differ diff --git a/libs/pytz/zoneinfo/Europe/Budapest b/libs/pytz/zoneinfo/Europe/Budapest new file mode 100644 index 00000000..dfdc6d24 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Budapest differ diff --git a/libs/pytz/zoneinfo/Europe/Busingen b/libs/pytz/zoneinfo/Europe/Busingen new file mode 100644 index 00000000..ad6cf592 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Busingen differ diff --git a/libs/pytz/zoneinfo/Europe/Chisinau b/libs/pytz/zoneinfo/Europe/Chisinau new file mode 100644 index 00000000..5bc1bfeb Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Chisinau differ diff --git a/libs/pytz/zoneinfo/Europe/Copenhagen b/libs/pytz/zoneinfo/Europe/Copenhagen new file mode 100644 index 00000000..cb2ec067 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Copenhagen differ diff --git a/libs/pytz/zoneinfo/Europe/Dublin b/libs/pytz/zoneinfo/Europe/Dublin new file mode 100644 index 00000000..5c5a7a3b Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Dublin differ diff --git a/libs/pytz/zoneinfo/Europe/Gibraltar b/libs/pytz/zoneinfo/Europe/Gibraltar new file mode 100644 index 00000000..117aadb8 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Gibraltar differ diff --git a/libs/pytz/zoneinfo/Europe/Guernsey b/libs/pytz/zoneinfo/Europe/Guernsey new file mode 100644 index 00000000..a340326e Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Guernsey differ diff --git a/libs/pytz/zoneinfo/Europe/Helsinki b/libs/pytz/zoneinfo/Europe/Helsinki new file mode 100644 index 00000000..b4f8f9cb Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Helsinki differ diff --git a/libs/pytz/zoneinfo/Europe/Isle_of_Man b/libs/pytz/zoneinfo/Europe/Isle_of_Man new file mode 100644 index 00000000..a340326e Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Isle_of_Man differ diff --git a/libs/pytz/zoneinfo/Europe/Istanbul b/libs/pytz/zoneinfo/Europe/Istanbul new file mode 100644 index 00000000..833d4eba Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Istanbul differ diff --git a/libs/pytz/zoneinfo/Europe/Jersey b/libs/pytz/zoneinfo/Europe/Jersey new file mode 100644 index 00000000..a340326e Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Jersey differ diff --git a/libs/pytz/zoneinfo/Europe/Kaliningrad b/libs/pytz/zoneinfo/Europe/Kaliningrad new file mode 100644 index 00000000..982d82a3 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Kaliningrad differ diff --git a/libs/pytz/zoneinfo/Europe/Kiev b/libs/pytz/zoneinfo/Europe/Kiev new file mode 100644 index 00000000..9337c9ea Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Kiev differ diff --git a/libs/pytz/zoneinfo/Europe/Kirov b/libs/pytz/zoneinfo/Europe/Kirov new file mode 100644 index 00000000..a3b5320a Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Kirov differ diff --git a/libs/pytz/zoneinfo/Europe/Lisbon b/libs/pytz/zoneinfo/Europe/Lisbon new file mode 100644 index 00000000..355817b5 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Lisbon differ diff --git a/libs/pytz/zoneinfo/Europe/Ljubljana b/libs/pytz/zoneinfo/Europe/Ljubljana new file mode 100644 index 00000000..32a57223 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Ljubljana differ diff --git a/libs/pytz/zoneinfo/Europe/London b/libs/pytz/zoneinfo/Europe/London new file mode 100644 index 00000000..a340326e Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/London differ diff --git a/libs/pytz/zoneinfo/Europe/Luxembourg b/libs/pytz/zoneinfo/Europe/Luxembourg new file mode 100644 index 00000000..6c194a5c Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Luxembourg differ diff --git a/libs/pytz/zoneinfo/Europe/Madrid b/libs/pytz/zoneinfo/Europe/Madrid new file mode 100644 index 00000000..ccc9d857 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Madrid differ diff --git a/libs/pytz/zoneinfo/Europe/Malta b/libs/pytz/zoneinfo/Europe/Malta new file mode 100644 index 00000000..bf2452da Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Malta differ diff --git a/libs/pytz/zoneinfo/Europe/Mariehamn b/libs/pytz/zoneinfo/Europe/Mariehamn new file mode 100644 index 00000000..b4f8f9cb Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Mariehamn differ diff --git a/libs/pytz/zoneinfo/Europe/Minsk b/libs/pytz/zoneinfo/Europe/Minsk new file mode 100644 index 00000000..801aead7 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Minsk differ diff --git a/libs/pytz/zoneinfo/Europe/Monaco b/libs/pytz/zoneinfo/Europe/Monaco new file mode 100644 index 00000000..686ae883 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Monaco differ diff --git a/libs/pytz/zoneinfo/Europe/Moscow b/libs/pytz/zoneinfo/Europe/Moscow new file mode 100644 index 00000000..ddb3f4e9 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Moscow differ diff --git a/libs/pytz/zoneinfo/Europe/Nicosia b/libs/pytz/zoneinfo/Europe/Nicosia new file mode 100644 index 00000000..f7f10ab7 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Nicosia differ diff --git a/libs/pytz/zoneinfo/Europe/Oslo b/libs/pytz/zoneinfo/Europe/Oslo new file mode 100644 index 00000000..c6842af8 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Oslo differ diff --git a/libs/pytz/zoneinfo/Europe/Paris b/libs/pytz/zoneinfo/Europe/Paris new file mode 100644 index 00000000..ca854351 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Paris differ diff --git a/libs/pytz/zoneinfo/Europe/Podgorica b/libs/pytz/zoneinfo/Europe/Podgorica new file mode 100644 index 00000000..32a57223 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Podgorica differ diff --git a/libs/pytz/zoneinfo/Europe/Prague b/libs/pytz/zoneinfo/Europe/Prague new file mode 100644 index 00000000..85036de3 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Prague differ diff --git a/libs/pytz/zoneinfo/Europe/Riga b/libs/pytz/zoneinfo/Europe/Riga new file mode 100644 index 00000000..8495c506 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Riga differ diff --git a/libs/pytz/zoneinfo/Europe/Rome b/libs/pytz/zoneinfo/Europe/Rome new file mode 100644 index 00000000..78a131b9 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Rome differ diff --git a/libs/pytz/zoneinfo/Europe/Samara b/libs/pytz/zoneinfo/Europe/Samara new file mode 100644 index 00000000..97d5dd9e Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Samara differ diff --git a/libs/pytz/zoneinfo/Europe/San_Marino b/libs/pytz/zoneinfo/Europe/San_Marino new file mode 100644 index 00000000..78a131b9 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/San_Marino differ diff --git a/libs/pytz/zoneinfo/Europe/Sarajevo b/libs/pytz/zoneinfo/Europe/Sarajevo new file mode 100644 index 00000000..32a57223 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Sarajevo differ diff --git a/libs/pytz/zoneinfo/Europe/Saratov b/libs/pytz/zoneinfo/Europe/Saratov new file mode 100644 index 00000000..8fd5f6d4 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Saratov differ diff --git a/libs/pytz/zoneinfo/Europe/Simferopol b/libs/pytz/zoneinfo/Europe/Simferopol new file mode 100644 index 00000000..e82dbbc7 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Simferopol differ diff --git a/libs/pytz/zoneinfo/Europe/Skopje b/libs/pytz/zoneinfo/Europe/Skopje new file mode 100644 index 00000000..32a57223 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Skopje differ diff --git a/libs/pytz/zoneinfo/Europe/Sofia b/libs/pytz/zoneinfo/Europe/Sofia new file mode 100644 index 00000000..dcfdd082 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Sofia differ diff --git a/libs/pytz/zoneinfo/Europe/Stockholm b/libs/pytz/zoneinfo/Europe/Stockholm new file mode 100644 index 00000000..f3e0c7f0 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Stockholm differ diff --git a/libs/pytz/zoneinfo/Europe/Tallinn b/libs/pytz/zoneinfo/Europe/Tallinn new file mode 100644 index 00000000..3a744cc6 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Tallinn differ diff --git a/libs/pytz/zoneinfo/Europe/Tirane b/libs/pytz/zoneinfo/Europe/Tirane new file mode 100644 index 00000000..0b86017d Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Tirane differ diff --git a/libs/pytz/zoneinfo/Europe/Tiraspol b/libs/pytz/zoneinfo/Europe/Tiraspol new file mode 100644 index 00000000..5bc1bfeb Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Tiraspol differ diff --git a/libs/pytz/zoneinfo/Europe/Ulyanovsk b/libs/pytz/zoneinfo/Europe/Ulyanovsk new file mode 100644 index 00000000..7b61bdc5 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Ulyanovsk differ diff --git a/libs/pytz/zoneinfo/Europe/Uzhgorod b/libs/pytz/zoneinfo/Europe/Uzhgorod new file mode 100644 index 00000000..677f0887 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Uzhgorod differ diff --git a/libs/pytz/zoneinfo/Europe/Vaduz b/libs/pytz/zoneinfo/Europe/Vaduz new file mode 100644 index 00000000..ad6cf592 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Vaduz differ diff --git a/libs/pytz/zoneinfo/Europe/Vatican b/libs/pytz/zoneinfo/Europe/Vatican new file mode 100644 index 00000000..78a131b9 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Vatican differ diff --git a/libs/pytz/zoneinfo/Europe/Vienna b/libs/pytz/zoneinfo/Europe/Vienna new file mode 100644 index 00000000..9e2d0c94 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Vienna differ diff --git a/libs/pytz/zoneinfo/Europe/Vilnius b/libs/pytz/zoneinfo/Europe/Vilnius new file mode 100644 index 00000000..46ce484f Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Vilnius differ diff --git a/libs/pytz/zoneinfo/Europe/Volgograd b/libs/pytz/zoneinfo/Europe/Volgograd new file mode 100644 index 00000000..8f170dd9 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Volgograd differ diff --git a/libs/pytz/zoneinfo/Europe/Warsaw b/libs/pytz/zoneinfo/Europe/Warsaw new file mode 100644 index 00000000..d6bb1561 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Warsaw differ diff --git a/libs/pytz/zoneinfo/Europe/Zagreb b/libs/pytz/zoneinfo/Europe/Zagreb new file mode 100644 index 00000000..32a57223 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Zagreb differ diff --git a/libs/pytz/zoneinfo/Europe/Zaporozhye b/libs/pytz/zoneinfo/Europe/Zaporozhye new file mode 100644 index 00000000..e42edfc8 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Zaporozhye differ diff --git a/libs/pytz/zoneinfo/Europe/Zurich b/libs/pytz/zoneinfo/Europe/Zurich new file mode 100644 index 00000000..ad6cf592 Binary files /dev/null and b/libs/pytz/zoneinfo/Europe/Zurich differ diff --git a/libs/pytz/zoneinfo/Factory b/libs/pytz/zoneinfo/Factory new file mode 100644 index 00000000..95bc3a3b Binary files /dev/null and b/libs/pytz/zoneinfo/Factory differ diff --git a/libs/pytz/zoneinfo/GB b/libs/pytz/zoneinfo/GB new file mode 100644 index 00000000..a340326e Binary files /dev/null and b/libs/pytz/zoneinfo/GB differ diff --git a/libs/pytz/zoneinfo/GB-Eire b/libs/pytz/zoneinfo/GB-Eire new file mode 100644 index 00000000..a340326e Binary files /dev/null and b/libs/pytz/zoneinfo/GB-Eire differ diff --git a/libs/pytz/zoneinfo/GMT b/libs/pytz/zoneinfo/GMT new file mode 100644 index 00000000..2ee14295 Binary files /dev/null and b/libs/pytz/zoneinfo/GMT differ diff --git a/libs/pytz/zoneinfo/GMT+0 b/libs/pytz/zoneinfo/GMT+0 new file mode 100644 index 00000000..2ee14295 Binary files /dev/null and b/libs/pytz/zoneinfo/GMT+0 differ diff --git a/libs/pytz/zoneinfo/GMT-0 b/libs/pytz/zoneinfo/GMT-0 new file mode 100644 index 00000000..2ee14295 Binary files /dev/null and b/libs/pytz/zoneinfo/GMT-0 differ diff --git a/libs/pytz/zoneinfo/GMT0 b/libs/pytz/zoneinfo/GMT0 new file mode 100644 index 00000000..2ee14295 Binary files /dev/null and b/libs/pytz/zoneinfo/GMT0 differ diff --git a/libs/pytz/zoneinfo/Greenwich b/libs/pytz/zoneinfo/Greenwich new file mode 100644 index 00000000..2ee14295 Binary files /dev/null and b/libs/pytz/zoneinfo/Greenwich differ diff --git a/libs/pytz/zoneinfo/HST b/libs/pytz/zoneinfo/HST new file mode 100644 index 00000000..616c31bc Binary files /dev/null and b/libs/pytz/zoneinfo/HST differ diff --git a/libs/pytz/zoneinfo/Hongkong b/libs/pytz/zoneinfo/Hongkong new file mode 100644 index 00000000..8e5c5813 Binary files /dev/null and b/libs/pytz/zoneinfo/Hongkong differ diff --git a/libs/pytz/zoneinfo/Iceland b/libs/pytz/zoneinfo/Iceland new file mode 100644 index 00000000..ac6bd697 Binary files /dev/null and b/libs/pytz/zoneinfo/Iceland differ diff --git a/libs/pytz/zoneinfo/Indian/Antananarivo b/libs/pytz/zoneinfo/Indian/Antananarivo new file mode 100644 index 00000000..6e19601f Binary files /dev/null and b/libs/pytz/zoneinfo/Indian/Antananarivo differ diff --git a/libs/pytz/zoneinfo/Indian/Chagos b/libs/pytz/zoneinfo/Indian/Chagos new file mode 100644 index 00000000..f609611c Binary files /dev/null and b/libs/pytz/zoneinfo/Indian/Chagos differ diff --git a/libs/pytz/zoneinfo/Indian/Christmas b/libs/pytz/zoneinfo/Indian/Christmas new file mode 100644 index 00000000..6babdeea Binary files /dev/null and b/libs/pytz/zoneinfo/Indian/Christmas differ diff --git a/libs/pytz/zoneinfo/Indian/Cocos b/libs/pytz/zoneinfo/Indian/Cocos new file mode 100644 index 00000000..58f80514 Binary files /dev/null and b/libs/pytz/zoneinfo/Indian/Cocos differ diff --git a/libs/pytz/zoneinfo/Indian/Comoro b/libs/pytz/zoneinfo/Indian/Comoro new file mode 100644 index 00000000..6e19601f Binary files /dev/null and b/libs/pytz/zoneinfo/Indian/Comoro differ diff --git a/libs/pytz/zoneinfo/Indian/Kerguelen b/libs/pytz/zoneinfo/Indian/Kerguelen new file mode 100644 index 00000000..2cb6f3e3 Binary files /dev/null and b/libs/pytz/zoneinfo/Indian/Kerguelen differ diff --git a/libs/pytz/zoneinfo/Indian/Mahe b/libs/pytz/zoneinfo/Indian/Mahe new file mode 100644 index 00000000..49e23e5a Binary files /dev/null and b/libs/pytz/zoneinfo/Indian/Mahe differ diff --git a/libs/pytz/zoneinfo/Indian/Maldives b/libs/pytz/zoneinfo/Indian/Maldives new file mode 100644 index 00000000..ffa33658 Binary files /dev/null and b/libs/pytz/zoneinfo/Indian/Maldives differ diff --git a/libs/pytz/zoneinfo/Indian/Mauritius b/libs/pytz/zoneinfo/Indian/Mauritius new file mode 100644 index 00000000..b23e2cee Binary files /dev/null and b/libs/pytz/zoneinfo/Indian/Mauritius differ diff --git a/libs/pytz/zoneinfo/Indian/Mayotte b/libs/pytz/zoneinfo/Indian/Mayotte new file mode 100644 index 00000000..6e19601f Binary files /dev/null and b/libs/pytz/zoneinfo/Indian/Mayotte differ diff --git a/libs/pytz/zoneinfo/Indian/Reunion b/libs/pytz/zoneinfo/Indian/Reunion new file mode 100644 index 00000000..11c6002e Binary files /dev/null and b/libs/pytz/zoneinfo/Indian/Reunion differ diff --git a/libs/pytz/zoneinfo/Iran b/libs/pytz/zoneinfo/Iran new file mode 100644 index 00000000..ad9058b4 Binary files /dev/null and b/libs/pytz/zoneinfo/Iran differ diff --git a/libs/pytz/zoneinfo/Israel b/libs/pytz/zoneinfo/Israel new file mode 100644 index 00000000..2d14c999 Binary files /dev/null and b/libs/pytz/zoneinfo/Israel differ diff --git a/libs/pytz/zoneinfo/Jamaica b/libs/pytz/zoneinfo/Jamaica new file mode 100644 index 00000000..162306f8 Binary files /dev/null and b/libs/pytz/zoneinfo/Jamaica differ diff --git a/libs/pytz/zoneinfo/Japan b/libs/pytz/zoneinfo/Japan new file mode 100644 index 00000000..26f4d34d Binary files /dev/null and b/libs/pytz/zoneinfo/Japan differ diff --git a/libs/pytz/zoneinfo/Kwajalein b/libs/pytz/zoneinfo/Kwajalein new file mode 100644 index 00000000..54bd71ff Binary files /dev/null and b/libs/pytz/zoneinfo/Kwajalein differ diff --git a/libs/pytz/zoneinfo/Libya b/libs/pytz/zoneinfo/Libya new file mode 100644 index 00000000..bd885315 Binary files /dev/null and b/libs/pytz/zoneinfo/Libya differ diff --git a/libs/pytz/zoneinfo/MET b/libs/pytz/zoneinfo/MET new file mode 100644 index 00000000..388dd744 Binary files /dev/null and b/libs/pytz/zoneinfo/MET differ diff --git a/libs/pytz/zoneinfo/MST b/libs/pytz/zoneinfo/MST new file mode 100644 index 00000000..da3e926d Binary files /dev/null and b/libs/pytz/zoneinfo/MST differ diff --git a/libs/pytz/zoneinfo/MST7MDT b/libs/pytz/zoneinfo/MST7MDT new file mode 100644 index 00000000..ddca8d19 Binary files /dev/null and b/libs/pytz/zoneinfo/MST7MDT differ diff --git a/libs/pytz/zoneinfo/Mexico/BajaNorte b/libs/pytz/zoneinfo/Mexico/BajaNorte new file mode 100644 index 00000000..ada6bf78 Binary files /dev/null and b/libs/pytz/zoneinfo/Mexico/BajaNorte differ diff --git a/libs/pytz/zoneinfo/Mexico/BajaSur b/libs/pytz/zoneinfo/Mexico/BajaSur new file mode 100644 index 00000000..43ee12d8 Binary files /dev/null and b/libs/pytz/zoneinfo/Mexico/BajaSur differ diff --git a/libs/pytz/zoneinfo/Mexico/General b/libs/pytz/zoneinfo/Mexico/General new file mode 100644 index 00000000..1434ab08 Binary files /dev/null and b/libs/pytz/zoneinfo/Mexico/General differ diff --git a/libs/pytz/zoneinfo/NZ b/libs/pytz/zoneinfo/NZ new file mode 100644 index 00000000..60bcef68 Binary files /dev/null and b/libs/pytz/zoneinfo/NZ differ diff --git a/libs/pytz/zoneinfo/NZ-CHAT b/libs/pytz/zoneinfo/NZ-CHAT new file mode 100644 index 00000000..abe09cb9 Binary files /dev/null and b/libs/pytz/zoneinfo/NZ-CHAT differ diff --git a/libs/pytz/zoneinfo/Navajo b/libs/pytz/zoneinfo/Navajo new file mode 100644 index 00000000..5fbe26b1 Binary files /dev/null and b/libs/pytz/zoneinfo/Navajo differ diff --git a/libs/pytz/zoneinfo/PRC b/libs/pytz/zoneinfo/PRC new file mode 100644 index 00000000..ce9e00a5 Binary files /dev/null and b/libs/pytz/zoneinfo/PRC differ diff --git a/libs/pytz/zoneinfo/PST8PDT b/libs/pytz/zoneinfo/PST8PDT new file mode 100644 index 00000000..d773e28f Binary files /dev/null and b/libs/pytz/zoneinfo/PST8PDT differ diff --git a/libs/pytz/zoneinfo/Pacific/Apia b/libs/pytz/zoneinfo/Pacific/Apia new file mode 100644 index 00000000..fd03ff76 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Apia differ diff --git a/libs/pytz/zoneinfo/Pacific/Auckland b/libs/pytz/zoneinfo/Pacific/Auckland new file mode 100644 index 00000000..60bcef68 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Auckland differ diff --git a/libs/pytz/zoneinfo/Pacific/Bougainville b/libs/pytz/zoneinfo/Pacific/Bougainville new file mode 100644 index 00000000..6a6c2da2 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Bougainville differ diff --git a/libs/pytz/zoneinfo/Pacific/Chatham b/libs/pytz/zoneinfo/Pacific/Chatham new file mode 100644 index 00000000..abe09cb9 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Chatham differ diff --git a/libs/pytz/zoneinfo/Pacific/Chuuk b/libs/pytz/zoneinfo/Pacific/Chuuk new file mode 100644 index 00000000..e79bca2d Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Chuuk differ diff --git a/libs/pytz/zoneinfo/Pacific/Easter b/libs/pytz/zoneinfo/Pacific/Easter new file mode 100644 index 00000000..cae37440 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Easter differ diff --git a/libs/pytz/zoneinfo/Pacific/Efate b/libs/pytz/zoneinfo/Pacific/Efate new file mode 100644 index 00000000..d650a056 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Efate differ diff --git a/libs/pytz/zoneinfo/Pacific/Enderbury b/libs/pytz/zoneinfo/Pacific/Enderbury new file mode 100644 index 00000000..80873503 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Enderbury differ diff --git a/libs/pytz/zoneinfo/Pacific/Fakaofo b/libs/pytz/zoneinfo/Pacific/Fakaofo new file mode 100644 index 00000000..4fa169f3 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Fakaofo differ diff --git a/libs/pytz/zoneinfo/Pacific/Fiji b/libs/pytz/zoneinfo/Pacific/Fiji new file mode 100644 index 00000000..61a66953 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Fiji differ diff --git a/libs/pytz/zoneinfo/Pacific/Funafuti b/libs/pytz/zoneinfo/Pacific/Funafuti new file mode 100644 index 00000000..e6a15447 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Funafuti differ diff --git a/libs/pytz/zoneinfo/Pacific/Galapagos b/libs/pytz/zoneinfo/Pacific/Galapagos new file mode 100644 index 00000000..859b76d9 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Galapagos differ diff --git a/libs/pytz/zoneinfo/Pacific/Gambier b/libs/pytz/zoneinfo/Pacific/Gambier new file mode 100644 index 00000000..4e9e36c5 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Gambier differ diff --git a/libs/pytz/zoneinfo/Pacific/Guadalcanal b/libs/pytz/zoneinfo/Pacific/Guadalcanal new file mode 100644 index 00000000..908ccc14 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Guadalcanal differ diff --git a/libs/pytz/zoneinfo/Pacific/Guam b/libs/pytz/zoneinfo/Pacific/Guam new file mode 100644 index 00000000..ffdf8c24 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Guam differ diff --git a/libs/pytz/zoneinfo/Pacific/Honolulu b/libs/pytz/zoneinfo/Pacific/Honolulu new file mode 100644 index 00000000..c7cd0601 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Honolulu differ diff --git a/libs/pytz/zoneinfo/Pacific/Johnston b/libs/pytz/zoneinfo/Pacific/Johnston new file mode 100644 index 00000000..c7cd0601 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Johnston differ diff --git a/libs/pytz/zoneinfo/Pacific/Kiritimati b/libs/pytz/zoneinfo/Pacific/Kiritimati new file mode 100644 index 00000000..cf5b3bd3 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Kiritimati differ diff --git a/libs/pytz/zoneinfo/Pacific/Kosrae b/libs/pytz/zoneinfo/Pacific/Kosrae new file mode 100644 index 00000000..b6bd4b08 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Kosrae differ diff --git a/libs/pytz/zoneinfo/Pacific/Kwajalein b/libs/pytz/zoneinfo/Pacific/Kwajalein new file mode 100644 index 00000000..54bd71ff Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Kwajalein differ diff --git a/libs/pytz/zoneinfo/Pacific/Majuro b/libs/pytz/zoneinfo/Pacific/Majuro new file mode 100644 index 00000000..53f32886 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Majuro differ diff --git a/libs/pytz/zoneinfo/Pacific/Marquesas b/libs/pytz/zoneinfo/Pacific/Marquesas new file mode 100644 index 00000000..5fad0e1b Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Marquesas differ diff --git a/libs/pytz/zoneinfo/Pacific/Midway b/libs/pytz/zoneinfo/Pacific/Midway new file mode 100644 index 00000000..72707b5e Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Midway differ diff --git a/libs/pytz/zoneinfo/Pacific/Nauru b/libs/pytz/zoneinfo/Pacific/Nauru new file mode 100644 index 00000000..7e7d920e Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Nauru differ diff --git a/libs/pytz/zoneinfo/Pacific/Niue b/libs/pytz/zoneinfo/Pacific/Niue new file mode 100644 index 00000000..1d58fe36 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Niue differ diff --git a/libs/pytz/zoneinfo/Pacific/Norfolk b/libs/pytz/zoneinfo/Pacific/Norfolk new file mode 100644 index 00000000..f630a65d Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Norfolk differ diff --git a/libs/pytz/zoneinfo/Pacific/Noumea b/libs/pytz/zoneinfo/Pacific/Noumea new file mode 100644 index 00000000..99f6bca2 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Noumea differ diff --git a/libs/pytz/zoneinfo/Pacific/Pago_Pago b/libs/pytz/zoneinfo/Pacific/Pago_Pago new file mode 100644 index 00000000..72707b5e Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Pago_Pago differ diff --git a/libs/pytz/zoneinfo/Pacific/Palau b/libs/pytz/zoneinfo/Pacific/Palau new file mode 100644 index 00000000..968f1956 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Palau differ diff --git a/libs/pytz/zoneinfo/Pacific/Pitcairn b/libs/pytz/zoneinfo/Pacific/Pitcairn new file mode 100644 index 00000000..9092e481 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Pitcairn differ diff --git a/libs/pytz/zoneinfo/Pacific/Pohnpei b/libs/pytz/zoneinfo/Pacific/Pohnpei new file mode 100644 index 00000000..d3393a20 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Pohnpei differ diff --git a/libs/pytz/zoneinfo/Pacific/Ponape b/libs/pytz/zoneinfo/Pacific/Ponape new file mode 100644 index 00000000..d3393a20 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Ponape differ diff --git a/libs/pytz/zoneinfo/Pacific/Port_Moresby b/libs/pytz/zoneinfo/Pacific/Port_Moresby new file mode 100644 index 00000000..f6fd51cb Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Port_Moresby differ diff --git a/libs/pytz/zoneinfo/Pacific/Rarotonga b/libs/pytz/zoneinfo/Pacific/Rarotonga new file mode 100644 index 00000000..9708b870 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Rarotonga differ diff --git a/libs/pytz/zoneinfo/Pacific/Saipan b/libs/pytz/zoneinfo/Pacific/Saipan new file mode 100644 index 00000000..ffdf8c24 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Saipan differ diff --git a/libs/pytz/zoneinfo/Pacific/Samoa b/libs/pytz/zoneinfo/Pacific/Samoa new file mode 100644 index 00000000..72707b5e Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Samoa differ diff --git a/libs/pytz/zoneinfo/Pacific/Tahiti b/libs/pytz/zoneinfo/Pacific/Tahiti new file mode 100644 index 00000000..37e4e883 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Tahiti differ diff --git a/libs/pytz/zoneinfo/Pacific/Tarawa b/libs/pytz/zoneinfo/Pacific/Tarawa new file mode 100644 index 00000000..e23c0cd2 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Tarawa differ diff --git a/libs/pytz/zoneinfo/Pacific/Tongatapu b/libs/pytz/zoneinfo/Pacific/Tongatapu new file mode 100644 index 00000000..35c9e2c6 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Tongatapu differ diff --git a/libs/pytz/zoneinfo/Pacific/Truk b/libs/pytz/zoneinfo/Pacific/Truk new file mode 100644 index 00000000..e79bca2d Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Truk differ diff --git a/libs/pytz/zoneinfo/Pacific/Wake b/libs/pytz/zoneinfo/Pacific/Wake new file mode 100644 index 00000000..837ce1f5 Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Wake differ diff --git a/libs/pytz/zoneinfo/Pacific/Wallis b/libs/pytz/zoneinfo/Pacific/Wallis new file mode 100644 index 00000000..8be9ac4d Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Wallis differ diff --git a/libs/pytz/zoneinfo/Pacific/Yap b/libs/pytz/zoneinfo/Pacific/Yap new file mode 100644 index 00000000..e79bca2d Binary files /dev/null and b/libs/pytz/zoneinfo/Pacific/Yap differ diff --git a/libs/pytz/zoneinfo/Poland b/libs/pytz/zoneinfo/Poland new file mode 100644 index 00000000..d6bb1561 Binary files /dev/null and b/libs/pytz/zoneinfo/Poland differ diff --git a/libs/pytz/zoneinfo/Portugal b/libs/pytz/zoneinfo/Portugal new file mode 100644 index 00000000..355817b5 Binary files /dev/null and b/libs/pytz/zoneinfo/Portugal differ diff --git a/libs/pytz/zoneinfo/ROC b/libs/pytz/zoneinfo/ROC new file mode 100644 index 00000000..f9cbe672 Binary files /dev/null and b/libs/pytz/zoneinfo/ROC differ diff --git a/libs/pytz/zoneinfo/ROK b/libs/pytz/zoneinfo/ROK new file mode 100644 index 00000000..fa1cbd39 Binary files /dev/null and b/libs/pytz/zoneinfo/ROK differ diff --git a/libs/pytz/zoneinfo/Singapore b/libs/pytz/zoneinfo/Singapore new file mode 100644 index 00000000..ebc4b0d9 Binary files /dev/null and b/libs/pytz/zoneinfo/Singapore differ diff --git a/libs/pytz/zoneinfo/Turkey b/libs/pytz/zoneinfo/Turkey new file mode 100644 index 00000000..833d4eba Binary files /dev/null and b/libs/pytz/zoneinfo/Turkey differ diff --git a/libs/pytz/zoneinfo/UCT b/libs/pytz/zoneinfo/UCT new file mode 100644 index 00000000..a88c4b66 Binary files /dev/null and b/libs/pytz/zoneinfo/UCT differ diff --git a/libs/pytz/zoneinfo/US/Alaska b/libs/pytz/zoneinfo/US/Alaska new file mode 100644 index 00000000..9bbb2fd3 Binary files /dev/null and b/libs/pytz/zoneinfo/US/Alaska differ diff --git a/libs/pytz/zoneinfo/US/Aleutian b/libs/pytz/zoneinfo/US/Aleutian new file mode 100644 index 00000000..43236498 Binary files /dev/null and b/libs/pytz/zoneinfo/US/Aleutian differ diff --git a/libs/pytz/zoneinfo/US/Arizona b/libs/pytz/zoneinfo/US/Arizona new file mode 100644 index 00000000..4d51271a Binary files /dev/null and b/libs/pytz/zoneinfo/US/Arizona differ diff --git a/libs/pytz/zoneinfo/US/Central b/libs/pytz/zoneinfo/US/Central new file mode 100644 index 00000000..a5b1617c Binary files /dev/null and b/libs/pytz/zoneinfo/US/Central differ diff --git a/libs/pytz/zoneinfo/US/East-Indiana b/libs/pytz/zoneinfo/US/East-Indiana new file mode 100644 index 00000000..09511ccd Binary files /dev/null and b/libs/pytz/zoneinfo/US/East-Indiana differ diff --git a/libs/pytz/zoneinfo/US/Eastern b/libs/pytz/zoneinfo/US/Eastern new file mode 100644 index 00000000..2f75480e Binary files /dev/null and b/libs/pytz/zoneinfo/US/Eastern differ diff --git a/libs/pytz/zoneinfo/US/Hawaii b/libs/pytz/zoneinfo/US/Hawaii new file mode 100644 index 00000000..c7cd0601 Binary files /dev/null and b/libs/pytz/zoneinfo/US/Hawaii differ diff --git a/libs/pytz/zoneinfo/US/Indiana-Starke b/libs/pytz/zoneinfo/US/Indiana-Starke new file mode 100644 index 00000000..fcd408d7 Binary files /dev/null and b/libs/pytz/zoneinfo/US/Indiana-Starke differ diff --git a/libs/pytz/zoneinfo/US/Michigan b/libs/pytz/zoneinfo/US/Michigan new file mode 100644 index 00000000..5e022605 Binary files /dev/null and b/libs/pytz/zoneinfo/US/Michigan differ diff --git a/libs/pytz/zoneinfo/US/Mountain b/libs/pytz/zoneinfo/US/Mountain new file mode 100644 index 00000000..5fbe26b1 Binary files /dev/null and b/libs/pytz/zoneinfo/US/Mountain differ diff --git a/libs/pytz/zoneinfo/US/Pacific b/libs/pytz/zoneinfo/US/Pacific new file mode 100644 index 00000000..9dad4f4c Binary files /dev/null and b/libs/pytz/zoneinfo/US/Pacific differ diff --git a/libs/pytz/zoneinfo/US/Samoa b/libs/pytz/zoneinfo/US/Samoa new file mode 100644 index 00000000..72707b5e Binary files /dev/null and b/libs/pytz/zoneinfo/US/Samoa differ diff --git a/libs/pytz/zoneinfo/UTC b/libs/pytz/zoneinfo/UTC new file mode 100644 index 00000000..5583f5b0 Binary files /dev/null and b/libs/pytz/zoneinfo/UTC differ diff --git a/libs/pytz/zoneinfo/Universal b/libs/pytz/zoneinfo/Universal new file mode 100644 index 00000000..5583f5b0 Binary files /dev/null and b/libs/pytz/zoneinfo/Universal differ diff --git a/libs/pytz/zoneinfo/W-SU b/libs/pytz/zoneinfo/W-SU new file mode 100644 index 00000000..ddb3f4e9 Binary files /dev/null and b/libs/pytz/zoneinfo/W-SU differ diff --git a/libs/pytz/zoneinfo/WET b/libs/pytz/zoneinfo/WET new file mode 100644 index 00000000..9b03a17f Binary files /dev/null and b/libs/pytz/zoneinfo/WET differ diff --git a/libs/pytz/zoneinfo/Zulu b/libs/pytz/zoneinfo/Zulu new file mode 100644 index 00000000..5583f5b0 Binary files /dev/null and b/libs/pytz/zoneinfo/Zulu differ diff --git a/libs/pytz/zoneinfo/iso3166.tab b/libs/pytz/zoneinfo/iso3166.tab new file mode 100644 index 00000000..c2e0f8ea --- /dev/null +++ b/libs/pytz/zoneinfo/iso3166.tab @@ -0,0 +1,274 @@ +# ISO 3166 alpha-2 country codes +# +# This file is in the public domain, so clarified as of +# 2009-05-17 by Arthur David Olson. +# +# From Paul Eggert (2015-05-02): +# This file contains a table of two-letter country codes. Columns are +# separated by a single tab. Lines beginning with '#' are comments. +# All text uses UTF-8 encoding. The columns of the table are as follows: +# +# 1. ISO 3166-1 alpha-2 country code, current as of +# ISO 3166-1 N905 (2016-11-15). See: Updates on ISO 3166-1 +# http://isotc.iso.org/livelink/livelink/Open/16944257 +# 2. The usual English name for the coded region, +# chosen so that alphabetic sorting of subsets produces helpful lists. +# This is not the same as the English name in the ISO 3166 tables. +# +# The table is sorted by country code. +# +# This table is intended as an aid for users, to help them select time +# zone data appropriate for their practical needs. It is not intended +# to take or endorse any position on legal or territorial claims. +# +#country- +#code name of country, territory, area, or subdivision +AD Andorra +AE United Arab Emirates +AF Afghanistan +AG Antigua & Barbuda +AI Anguilla +AL Albania +AM Armenia +AO Angola +AQ Antarctica +AR Argentina +AS Samoa (American) +AT Austria +AU Australia +AW Aruba +AX Åland Islands +AZ Azerbaijan +BA Bosnia & Herzegovina +BB Barbados +BD Bangladesh +BE Belgium +BF Burkina Faso +BG Bulgaria +BH Bahrain +BI Burundi +BJ Benin +BL St Barthelemy +BM Bermuda +BN Brunei +BO Bolivia +BQ Caribbean NL +BR Brazil +BS Bahamas +BT Bhutan +BV Bouvet Island +BW Botswana +BY Belarus +BZ Belize +CA Canada +CC Cocos (Keeling) Islands +CD Congo (Dem. Rep.) +CF Central African Rep. +CG Congo (Rep.) +CH Switzerland +CI Côte d'Ivoire +CK Cook Islands +CL Chile +CM Cameroon +CN China +CO Colombia +CR Costa Rica +CU Cuba +CV Cape Verde +CW Curaçao +CX Christmas Island +CY Cyprus +CZ Czech Republic +DE Germany +DJ Djibouti +DK Denmark +DM Dominica +DO Dominican Republic +DZ Algeria +EC Ecuador +EE Estonia +EG Egypt +EH Western Sahara +ER Eritrea +ES Spain +ET Ethiopia +FI Finland +FJ Fiji +FK Falkland Islands +FM Micronesia +FO Faroe Islands +FR France +GA Gabon +GB Britain (UK) +GD Grenada +GE Georgia +GF French Guiana +GG Guernsey +GH Ghana +GI Gibraltar +GL Greenland +GM Gambia +GN Guinea +GP Guadeloupe +GQ Equatorial Guinea +GR Greece +GS South Georgia & the South Sandwich Islands +GT Guatemala +GU Guam +GW Guinea-Bissau +GY Guyana +HK Hong Kong +HM Heard Island & McDonald Islands +HN Honduras +HR Croatia +HT Haiti +HU Hungary +ID Indonesia +IE Ireland +IL Israel +IM Isle of Man +IN India +IO British Indian Ocean Territory +IQ Iraq +IR Iran +IS Iceland +IT Italy +JE Jersey +JM Jamaica +JO Jordan +JP Japan +KE Kenya +KG Kyrgyzstan +KH Cambodia +KI Kiribati +KM Comoros +KN St Kitts & Nevis +KP Korea (North) +KR Korea (South) +KW Kuwait +KY Cayman Islands +KZ Kazakhstan +LA Laos +LB Lebanon +LC St Lucia +LI Liechtenstein +LK Sri Lanka +LR Liberia +LS Lesotho +LT Lithuania +LU Luxembourg +LV Latvia +LY Libya +MA Morocco +MC Monaco +MD Moldova +ME Montenegro +MF St Martin (French) +MG Madagascar +MH Marshall Islands +MK Macedonia +ML Mali +MM Myanmar (Burma) +MN Mongolia +MO Macau +MP Northern Mariana Islands +MQ Martinique +MR Mauritania +MS Montserrat +MT Malta +MU Mauritius +MV Maldives +MW Malawi +MX Mexico +MY Malaysia +MZ Mozambique +NA Namibia +NC New Caledonia +NE Niger +NF Norfolk Island +NG Nigeria +NI Nicaragua +NL Netherlands +NO Norway +NP Nepal +NR Nauru +NU Niue +NZ New Zealand +OM Oman +PA Panama +PE Peru +PF French Polynesia +PG Papua New Guinea +PH Philippines +PK Pakistan +PL Poland +PM St Pierre & Miquelon +PN Pitcairn +PR Puerto Rico +PS Palestine +PT Portugal +PW Palau +PY Paraguay +QA Qatar +RE Réunion +RO Romania +RS Serbia +RU Russia +RW Rwanda +SA Saudi Arabia +SB Solomon Islands +SC Seychelles +SD Sudan +SE Sweden +SG Singapore +SH St Helena +SI Slovenia +SJ Svalbard & Jan Mayen +SK Slovakia +SL Sierra Leone +SM San Marino +SN Senegal +SO Somalia +SR Suriname +SS South Sudan +ST Sao Tome & Principe +SV El Salvador +SX St Maarten (Dutch) +SY Syria +SZ Swaziland +TC Turks & Caicos Is +TD Chad +TF French Southern & Antarctic Lands +TG Togo +TH Thailand +TJ Tajikistan +TK Tokelau +TL East Timor +TM Turkmenistan +TN Tunisia +TO Tonga +TR Turkey +TT Trinidad & Tobago +TV Tuvalu +TW Taiwan +TZ Tanzania +UA Ukraine +UG Uganda +UM US minor outlying islands +US United States +UY Uruguay +UZ Uzbekistan +VA Vatican City +VC St Vincent +VE Venezuela +VG Virgin Islands (UK) +VI Virgin Islands (US) +VN Vietnam +VU Vanuatu +WF Wallis & Futuna +WS Samoa (western) +YE Yemen +YT Mayotte +ZA South Africa +ZM Zambia +ZW Zimbabwe diff --git a/libs/pytz/zoneinfo/leapseconds b/libs/pytz/zoneinfo/leapseconds new file mode 100644 index 00000000..148aa8ee --- /dev/null +++ b/libs/pytz/zoneinfo/leapseconds @@ -0,0 +1,66 @@ +# Allowance for leap seconds added to each time zone file. + +# This file is in the public domain. + +# This file is generated automatically from the data in the public-domain +# leap-seconds.list file, which can be copied from +# +# or +# or . +# For more about leap-seconds.list, please see +# The NTP Timescale and Leap Seconds +# . + +# The International Earth Rotation and Reference Systems Service +# periodically uses leap seconds to keep UTC to within 0.9 s of UT1 +# (which measures the true angular orientation of the earth in space) +# and publishes leap second data in a copyrighted file +# . +# See: Levine J. Coordinated Universal Time and the leap second. +# URSI Radio Sci Bull. 2016;89(4):30-6. doi:10.23919/URSIRSB.2016.7909995 +# . +# There were no leap seconds before 1972, because the official mechanism +# accounting for the discrepancy between atomic time and the earth's rotation +# did not exist. + +# The correction (+ or -) is made at the given time, so lines +# will typically look like: +# Leap YEAR MON DAY 23:59:60 + R/S +# or +# Leap YEAR MON DAY 23:59:59 - R/S + +# If the leap second is Rolling (R) the given time is local time (unused here). +Leap 1972 Jun 30 23:59:60 + S +Leap 1972 Dec 31 23:59:60 + S +Leap 1973 Dec 31 23:59:60 + S +Leap 1974 Dec 31 23:59:60 + S +Leap 1975 Dec 31 23:59:60 + S +Leap 1976 Dec 31 23:59:60 + S +Leap 1977 Dec 31 23:59:60 + S +Leap 1978 Dec 31 23:59:60 + S +Leap 1979 Dec 31 23:59:60 + S +Leap 1981 Jun 30 23:59:60 + S +Leap 1982 Jun 30 23:59:60 + S +Leap 1983 Jun 30 23:59:60 + S +Leap 1985 Jun 30 23:59:60 + S +Leap 1987 Dec 31 23:59:60 + S +Leap 1989 Dec 31 23:59:60 + S +Leap 1990 Dec 31 23:59:60 + S +Leap 1992 Jun 30 23:59:60 + S +Leap 1993 Jun 30 23:59:60 + S +Leap 1994 Jun 30 23:59:60 + S +Leap 1995 Dec 31 23:59:60 + S +Leap 1997 Jun 30 23:59:60 + S +Leap 1998 Dec 31 23:59:60 + S +Leap 2005 Dec 31 23:59:60 + S +Leap 2008 Dec 31 23:59:60 + S +Leap 2012 Jun 30 23:59:60 + S +Leap 2015 Jun 30 23:59:60 + S +Leap 2016 Dec 31 23:59:60 + S + +# POSIX timestamps for the data in this file: +#updated 1467936000 +#expires 1561680000 + +# Updated through IERS Bulletin C56 +# File expires on: 28 June 2019 diff --git a/libs/pytz/zoneinfo/posixrules b/libs/pytz/zoneinfo/posixrules new file mode 100644 index 00000000..2f75480e Binary files /dev/null and b/libs/pytz/zoneinfo/posixrules differ diff --git a/libs/pytz/zoneinfo/tzdata.zi b/libs/pytz/zoneinfo/tzdata.zi new file mode 100644 index 00000000..7eb8193b --- /dev/null +++ b/libs/pytz/zoneinfo/tzdata.zi @@ -0,0 +1,4177 @@ +# version unknown +# This zic input file is in the public domain. +R d 1916 o - Jun 14 23s 1 S +R d 1916 1919 - O Sun>=1 23s 0 - +R d 1917 o - Mar 24 23s 1 S +R d 1918 o - Mar 9 23s 1 S +R d 1919 o - Mar 1 23s 1 S +R d 1920 o - F 14 23s 1 S +R d 1920 o - O 23 23s 0 - +R d 1921 o - Mar 14 23s 1 S +R d 1921 o - Jun 21 23s 0 - +R d 1939 o - S 11 23s 1 S +R d 1939 o - N 19 1 0 - +R d 1944 1945 - Ap M>=1 2 1 S +R d 1944 o - O 8 2 0 - +R d 1945 o - S 16 1 0 - +R d 1971 o - Ap 25 23s 1 S +R d 1971 o - S 26 23s 0 - +R d 1977 o - May 6 0 1 S +R d 1977 o - O 21 0 0 - +R d 1978 o - Mar 24 1 1 S +R d 1978 o - S 22 3 0 - +R d 1980 o - Ap 25 0 1 S +R d 1980 o - O 31 2 0 - +Z Africa/Algiers 0:12:12 - LMT 1891 Mar 15 0:1 +0:9:21 - PMT 1911 Mar 11 +0 d WE%sT 1940 F 25 2 +1 d CE%sT 1946 O 7 +0 - WET 1956 Ja 29 +1 - CET 1963 Ap 14 +0 d WE%sT 1977 O 21 +1 d CE%sT 1979 O 26 +0 d WE%sT 1981 May +1 - CET +Z Atlantic/Cape_Verde -1:34:4 - LMT 1912 Ja 1 2u +-2 - -02 1942 S +-2 1 -01 1945 O 15 +-2 - -02 1975 N 25 2 +-1 - -01 +Z Africa/Ndjamena 1:0:12 - LMT 1912 +1 - WAT 1979 O 14 +1 1 WAST 1980 Mar 8 +1 - WAT +Z Africa/Abidjan -0:16:8 - LMT 1912 +0 - GMT +Li Africa/Abidjan Africa/Bamako +Li Africa/Abidjan Africa/Banjul +Li Africa/Abidjan Africa/Conakry +Li Africa/Abidjan Africa/Dakar +Li Africa/Abidjan Africa/Freetown +Li Africa/Abidjan Africa/Lome +Li Africa/Abidjan Africa/Nouakchott +Li Africa/Abidjan Africa/Ouagadougou +Li Africa/Abidjan Atlantic/St_Helena +R K 1940 o - Jul 15 0 1 S +R K 1940 o - O 1 0 0 - +R K 1941 o - Ap 15 0 1 S +R K 1941 o - S 16 0 0 - +R K 1942 1944 - Ap 1 0 1 S +R K 1942 o - O 27 0 0 - +R K 1943 1945 - N 1 0 0 - +R K 1945 o - Ap 16 0 1 S +R K 1957 o - May 10 0 1 S +R K 1957 1958 - O 1 0 0 - +R K 1958 o - May 1 0 1 S +R K 1959 1981 - May 1 1 1 S +R K 1959 1965 - S 30 3 0 - +R K 1966 1994 - O 1 3 0 - +R K 1982 o - Jul 25 1 1 S +R K 1983 o - Jul 12 1 1 S +R K 1984 1988 - May 1 1 1 S +R K 1989 o - May 6 1 1 S +R K 1990 1994 - May 1 1 1 S +R K 1995 2010 - Ap lastF 0s 1 S +R K 1995 2005 - S lastTh 24 0 - +R K 2006 o - S 21 24 0 - +R K 2007 o - S Th>=1 24 0 - +R K 2008 o - Au lastTh 24 0 - +R K 2009 o - Au 20 24 0 - +R K 2010 o - Au 10 24 0 - +R K 2010 o - S 9 24 1 S +R K 2010 o - S lastTh 24 0 - +R K 2014 o - May 15 24 1 S +R K 2014 o - Jun 26 24 0 - +R K 2014 o - Jul 31 24 1 S +R K 2014 o - S lastTh 24 0 - +Z Africa/Cairo 2:5:9 - LMT 1900 O +2 K EE%sT +R GH 1920 1942 - S 1 0 0:20 - +R GH 1920 1942 - D 31 0 0 - +Z Africa/Accra -0:0:52 - LMT 1918 +0 GH GMT/+0020 +Z Africa/Bissau -1:2:20 - LMT 1912 Ja 1 1u +-1 - -01 1975 +0 - GMT +Z Africa/Nairobi 2:27:16 - LMT 1928 Jul +3 - EAT 1930 +2:30 - +0230 1940 +2:45 - +0245 1960 +3 - EAT +Li Africa/Nairobi Africa/Addis_Ababa +Li Africa/Nairobi Africa/Asmara +Li Africa/Nairobi Africa/Dar_es_Salaam +Li Africa/Nairobi Africa/Djibouti +Li Africa/Nairobi Africa/Kampala +Li Africa/Nairobi Africa/Mogadishu +Li Africa/Nairobi Indian/Antananarivo +Li Africa/Nairobi Indian/Comoro +Li Africa/Nairobi Indian/Mayotte +Z Africa/Monrovia -0:43:8 - LMT 1882 +-0:43:8 - MMT 1919 Mar +-0:44:30 - MMT 1972 Ja 7 +0 - GMT +R L 1951 o - O 14 2 1 S +R L 1952 o - Ja 1 0 0 - +R L 1953 o - O 9 2 1 S +R L 1954 o - Ja 1 0 0 - +R L 1955 o - S 30 0 1 S +R L 1956 o - Ja 1 0 0 - +R L 1982 1984 - Ap 1 0 1 S +R L 1982 1985 - O 1 0 0 - +R L 1985 o - Ap 6 0 1 S +R L 1986 o - Ap 4 0 1 S +R L 1986 o - O 3 0 0 - +R L 1987 1989 - Ap 1 0 1 S +R L 1987 1989 - O 1 0 0 - +R L 1997 o - Ap 4 0 1 S +R L 1997 o - O 4 0 0 - +R L 2013 o - Mar lastF 1 1 S +R L 2013 o - O lastF 2 0 - +Z Africa/Tripoli 0:52:44 - LMT 1920 +1 L CE%sT 1959 +2 - EET 1982 +1 L CE%sT 1990 May 4 +2 - EET 1996 S 30 +1 L CE%sT 1997 O 4 +2 - EET 2012 N 10 2 +1 L CE%sT 2013 O 25 2 +2 - EET +R MU 1982 o - O 10 0 1 - +R MU 1983 o - Mar 21 0 0 - +R MU 2008 o - O lastSun 2 1 - +R MU 2009 o - Mar lastSun 2 0 - +Z Indian/Mauritius 3:50 - LMT 1907 +4 MU +04/+05 +R M 1939 o - S 12 0 1 - +R M 1939 o - N 19 0 0 - +R M 1940 o - F 25 0 1 - +R M 1945 o - N 18 0 0 - +R M 1950 o - Jun 11 0 1 - +R M 1950 o - O 29 0 0 - +R M 1967 o - Jun 3 12 1 - +R M 1967 o - O 1 0 0 - +R M 1974 o - Jun 24 0 1 - +R M 1974 o - S 1 0 0 - +R M 1976 1977 - May 1 0 1 - +R M 1976 o - Au 1 0 0 - +R M 1977 o - S 28 0 0 - +R M 1978 o - Jun 1 0 1 - +R M 1978 o - Au 4 0 0 - +R M 2008 o - Jun 1 0 1 - +R M 2008 o - S 1 0 0 - +R M 2009 o - Jun 1 0 1 - +R M 2009 o - Au 21 0 0 - +R M 2010 o - May 2 0 1 - +R M 2010 o - Au 8 0 0 - +R M 2011 o - Ap 3 0 1 - +R M 2011 o - Jul 31 0 0 - +R M 2012 2013 - Ap lastSun 2 1 - +R M 2012 o - Jul 20 3 0 - +R M 2012 o - Au 20 2 1 - +R M 2012 o - S 30 3 0 - +R M 2013 o - Jul 7 3 0 - +R M 2013 o - Au 10 2 1 - +R M 2013 2018 - O lastSun 3 0 - +R M 2014 2018 - Mar lastSun 2 1 - +R M 2014 o - Jun 28 3 0 - +R M 2014 o - Au 2 2 1 - +R M 2015 o - Jun 14 3 0 - +R M 2015 o - Jul 19 2 1 - +R M 2016 o - Jun 5 3 0 - +R M 2016 o - Jul 10 2 1 - +R M 2017 o - May 21 3 0 - +R M 2017 o - Jul 2 2 1 - +R M 2018 o - May 13 3 0 - +R M 2018 o - Jun 17 2 1 - +Z Africa/Casablanca -0:30:20 - LMT 1913 O 26 +0 M +00/+01 1984 Mar 16 +1 - +01 1986 +0 M +00/+01 2018 O 27 +1 - +01 +Z Africa/El_Aaiun -0:52:48 - LMT 1934 +-1 - -01 1976 Ap 14 +0 M +00/+01 2018 O 27 +1 - +01 +Z Africa/Maputo 2:10:20 - LMT 1903 Mar +2 - CAT +Li Africa/Maputo Africa/Blantyre +Li Africa/Maputo Africa/Bujumbura +Li Africa/Maputo Africa/Gaborone +Li Africa/Maputo Africa/Harare +Li Africa/Maputo Africa/Kigali +Li Africa/Maputo Africa/Lubumbashi +Li Africa/Maputo Africa/Lusaka +R NA 1994 o - Mar 21 0 -1 WAT +R NA 1994 2017 - S Sun>=1 2 0 CAT +R NA 1995 2017 - Ap Sun>=1 2 -1 WAT +Z Africa/Windhoek 1:8:24 - LMT 1892 F 8 +1:30 - +0130 1903 Mar +2 - SAST 1942 S 20 2 +2 1 SAST 1943 Mar 21 2 +2 - SAST 1990 Mar 21 +2 NA %s +Z Africa/Lagos 0:13:36 - LMT 1919 S +1 - WAT +Li Africa/Lagos Africa/Bangui +Li Africa/Lagos Africa/Brazzaville +Li Africa/Lagos Africa/Douala +Li Africa/Lagos Africa/Kinshasa +Li Africa/Lagos Africa/Libreville +Li Africa/Lagos Africa/Luanda +Li Africa/Lagos Africa/Malabo +Li Africa/Lagos Africa/Niamey +Li Africa/Lagos Africa/Porto-Novo +Z Indian/Reunion 3:41:52 - LMT 1911 Jun +4 - +04 +Z Africa/Sao_Tome 0:26:56 - LMT 1884 +-0:36:45 - LMT 1912 Ja 1 0u +0 - GMT 2018 Ja 1 1 +1 - WAT +Z Indian/Mahe 3:41:48 - LMT 1906 Jun +4 - +04 +R SA 1942 1943 - S Sun>=15 2 1 - +R SA 1943 1944 - Mar Sun>=15 2 0 - +Z Africa/Johannesburg 1:52 - LMT 1892 F 8 +1:30 - SAST 1903 Mar +2 SA SAST +Li Africa/Johannesburg Africa/Maseru +Li Africa/Johannesburg Africa/Mbabane +R SD 1970 o - May 1 0 1 S +R SD 1970 1985 - O 15 0 0 - +R SD 1971 o - Ap 30 0 1 S +R SD 1972 1985 - Ap lastSun 0 1 S +Z Africa/Khartoum 2:10:8 - LMT 1931 +2 SD CA%sT 2000 Ja 15 12 +3 - EAT 2017 N +2 - CAT +Z Africa/Juba 2:6:28 - LMT 1931 +2 SD CA%sT 2000 Ja 15 12 +3 - EAT +R n 1939 o - Ap 15 23s 1 S +R n 1939 o - N 18 23s 0 - +R n 1940 o - F 25 23s 1 S +R n 1941 o - O 6 0 0 - +R n 1942 o - Mar 9 0 1 S +R n 1942 o - N 2 3 0 - +R n 1943 o - Mar 29 2 1 S +R n 1943 o - Ap 17 2 0 - +R n 1943 o - Ap 25 2 1 S +R n 1943 o - O 4 2 0 - +R n 1944 1945 - Ap M>=1 2 1 S +R n 1944 o - O 8 0 0 - +R n 1945 o - S 16 0 0 - +R n 1977 o - Ap 30 0s 1 S +R n 1977 o - S 24 0s 0 - +R n 1978 o - May 1 0s 1 S +R n 1978 o - O 1 0s 0 - +R n 1988 o - Jun 1 0s 1 S +R n 1988 1990 - S lastSun 0s 0 - +R n 1989 o - Mar 26 0s 1 S +R n 1990 o - May 1 0s 1 S +R n 2005 o - May 1 0s 1 S +R n 2005 o - S 30 1s 0 - +R n 2006 2008 - Mar lastSun 2s 1 S +R n 2006 2008 - O lastSun 2s 0 - +Z Africa/Tunis 0:40:44 - LMT 1881 May 12 +0:9:21 - PMT 1911 Mar 11 +1 n CE%sT +Z Antarctica/Casey 0 - -00 1969 +8 - +08 2009 O 18 2 +11 - +11 2010 Mar 5 2 +8 - +08 2011 O 28 2 +11 - +11 2012 F 21 17u +8 - +08 2016 O 22 +11 - +11 2018 Mar 11 4 +8 - +08 +Z Antarctica/Davis 0 - -00 1957 Ja 13 +7 - +07 1964 N +0 - -00 1969 F +7 - +07 2009 O 18 2 +5 - +05 2010 Mar 10 20u +7 - +07 2011 O 28 2 +5 - +05 2012 F 21 20u +7 - +07 +Z Antarctica/Mawson 0 - -00 1954 F 13 +6 - +06 2009 O 18 2 +5 - +05 +Z Indian/Kerguelen 0 - -00 1950 +5 - +05 +Z Antarctica/DumontDUrville 0 - -00 1947 +10 - +10 1952 Ja 14 +0 - -00 1956 N +10 - +10 +Z Antarctica/Syowa 0 - -00 1957 Ja 29 +3 - +03 +R Tr 2005 ma - Mar lastSun 1u 2 +02 +R Tr 2004 ma - O lastSun 1u 0 +00 +Z Antarctica/Troll 0 - -00 2005 F 12 +0 Tr %s +Z Antarctica/Vostok 0 - -00 1957 D 16 +6 - +06 +Z Antarctica/Rothera 0 - -00 1976 D +-3 - -03 +Z Asia/Kabul 4:36:48 - LMT 1890 +4 - +04 1945 +4:30 - +0430 +R AM 2011 o - Mar lastSun 2s 1 - +R AM 2011 o - O lastSun 2s 0 - +Z Asia/Yerevan 2:58 - LMT 1924 May 2 +3 - +03 1957 Mar +4 R +04/+05 1991 Mar 31 2s +3 R +03/+04 1995 S 24 2s +4 - +04 1997 +4 R +04/+05 2011 +4 AM +04/+05 +R AZ 1997 2015 - Mar lastSun 4 1 - +R AZ 1997 2015 - O lastSun 5 0 - +Z Asia/Baku 3:19:24 - LMT 1924 May 2 +3 - +03 1957 Mar +4 R +04/+05 1991 Mar 31 2s +3 R +03/+04 1992 S lastSun 2s +4 - +04 1996 +4 E +04/+05 1997 +4 AZ +04/+05 +R BD 2009 o - Jun 19 23 1 - +R BD 2009 o - D 31 24 0 - +Z Asia/Dhaka 6:1:40 - LMT 1890 +5:53:20 - HMT 1941 O +6:30 - +0630 1942 May 15 +5:30 - +0530 1942 S +6:30 - +0630 1951 S 30 +6 - +06 2009 +6 BD +06/+07 +Z Asia/Thimphu 5:58:36 - LMT 1947 Au 15 +5:30 - +0530 1987 O +6 - +06 +Z Indian/Chagos 4:49:40 - LMT 1907 +5 - +05 1996 +6 - +06 +Z Asia/Brunei 7:39:40 - LMT 1926 Mar +7:30 - +0730 1933 +8 - +08 +Z Asia/Yangon 6:24:47 - LMT 1880 +6:24:47 - RMT 1920 +6:30 - +0630 1942 May +9 - +09 1945 May 3 +6:30 - +0630 +R Sh 1940 o - Jun 1 0 1 D +R Sh 1940 o - O 12 24 0 S +R Sh 1941 o - Mar 15 0 1 D +R Sh 1941 o - N 1 24 0 S +R Sh 1942 o - Ja 31 0 1 D +R Sh 1945 o - S 1 24 0 S +R Sh 1946 o - May 15 0 1 D +R Sh 1946 o - S 30 24 0 S +R Sh 1947 o - Ap 15 0 1 D +R Sh 1947 o - O 31 24 0 S +R Sh 1948 1949 - May 1 0 1 D +R Sh 1948 1949 - S 30 24 0 S +R CN 1986 o - May 4 2 1 D +R CN 1986 1991 - S Sun>=11 2 0 S +R CN 1987 1991 - Ap Sun>=11 2 1 D +Z Asia/Shanghai 8:5:43 - LMT 1901 +8 Sh C%sT 1949 May 28 +8 CN C%sT +Z Asia/Urumqi 5:50:20 - LMT 1928 +6 - +06 +R HK 1941 o - Ap 1 3:30 1 S +R HK 1941 o - S 30 3:30 0 - +R HK 1946 o - Ap 20 3:30 1 S +R HK 1946 o - D 1 3:30 0 - +R HK 1947 o - Ap 13 3:30 1 S +R HK 1947 o - D 30 3:30 0 - +R HK 1948 o - May 2 3:30 1 S +R HK 1948 1951 - O lastSun 3:30 0 - +R HK 1952 o - O 25 3:30 0 - +R HK 1949 1953 - Ap Sun>=1 3:30 1 S +R HK 1953 o - N 1 3:30 0 - +R HK 1954 1964 - Mar Sun>=18 3:30 1 S +R HK 1954 o - O 31 3:30 0 - +R HK 1955 1964 - N Sun>=1 3:30 0 - +R HK 1965 1976 - Ap Sun>=16 3:30 1 S +R HK 1965 1976 - O Sun>=16 3:30 0 - +R HK 1973 o - D 30 3:30 1 S +R HK 1979 o - May Sun>=8 3:30 1 S +R HK 1979 o - O Sun>=16 3:30 0 - +Z Asia/Hong_Kong 7:36:42 - LMT 1904 O 30 +8 HK HK%sT 1941 D 25 +9 - JST 1945 S 15 +8 HK HK%sT +R f 1946 o - May 15 0 1 D +R f 1946 o - O 1 0 0 S +R f 1947 o - Ap 15 0 1 D +R f 1947 o - N 1 0 0 S +R f 1948 1951 - May 1 0 1 D +R f 1948 1951 - O 1 0 0 S +R f 1952 o - Mar 1 0 1 D +R f 1952 1954 - N 1 0 0 S +R f 1953 1959 - Ap 1 0 1 D +R f 1955 1961 - O 1 0 0 S +R f 1960 1961 - Jun 1 0 1 D +R f 1974 1975 - Ap 1 0 1 D +R f 1974 1975 - O 1 0 0 S +R f 1979 o - Jul 1 0 1 D +R f 1979 o - O 1 0 0 S +Z Asia/Taipei 8:6 - LMT 1896 +8 - CST 1937 O +9 - JST 1945 S 21 1 +8 f C%sT +R _ 1942 1943 - Ap 30 23 1 - +R _ 1942 o - N 17 23 0 - +R _ 1943 o - S 30 23 0 S +R _ 1946 o - Ap 30 23s 1 D +R _ 1946 o - S 30 23s 0 S +R _ 1947 o - Ap 19 23s 1 D +R _ 1947 o - N 30 23s 0 S +R _ 1948 o - May 2 23s 1 D +R _ 1948 o - O 31 23s 0 S +R _ 1949 1950 - Ap Sat>=1 23s 1 D +R _ 1949 1950 - O lastSat 23s 0 S +R _ 1951 o - Mar 31 23s 1 D +R _ 1951 o - O 28 23s 0 S +R _ 1952 1953 - Ap Sat>=1 23s 1 D +R _ 1952 o - N 1 23s 0 S +R _ 1953 1954 - O lastSat 23s 0 S +R _ 1954 1956 - Mar Sat>=17 23s 1 D +R _ 1955 o - N 5 23s 0 S +R _ 1956 1964 - N Sun>=1 3:30 0 S +R _ 1957 1964 - Mar Sun>=18 3:30 1 D +R _ 1965 1973 - Ap Sun>=16 3:30 1 D +R _ 1965 1966 - O Sun>=16 2:30 0 S +R _ 1967 1976 - O Sun>=16 3:30 0 S +R _ 1973 o - D 30 3:30 1 D +R _ 1975 1976 - Ap Sun>=16 3:30 1 D +R _ 1979 o - May 13 3:30 1 D +R _ 1979 o - O Sun>=16 3:30 0 S +Z Asia/Macau 7:34:10 - LMT 1904 O 30 +8 - CST 1941 D 21 23 +9 _ +09/+10 1945 S 30 24 +8 _ C%sT +R CY 1975 o - Ap 13 0 1 S +R CY 1975 o - O 12 0 0 - +R CY 1976 o - May 15 0 1 S +R CY 1976 o - O 11 0 0 - +R CY 1977 1980 - Ap Sun>=1 0 1 S +R CY 1977 o - S 25 0 0 - +R CY 1978 o - O 2 0 0 - +R CY 1979 1997 - S lastSun 0 0 - +R CY 1981 1998 - Mar lastSun 0 1 S +Z Asia/Nicosia 2:13:28 - LMT 1921 N 14 +2 CY EE%sT 1998 S +2 E EE%sT +Z Asia/Famagusta 2:15:48 - LMT 1921 N 14 +2 CY EE%sT 1998 S +2 E EE%sT 2016 S 8 +3 - +03 2017 O 29 1u +2 E EE%sT +Li Asia/Nicosia Europe/Nicosia +Z Asia/Tbilisi 2:59:11 - LMT 1880 +2:59:11 - TBMT 1924 May 2 +3 - +03 1957 Mar +4 R +04/+05 1991 Mar 31 2s +3 R +03/+04 1992 +3 e +03/+04 1994 S lastSun +4 e +04/+05 1996 O lastSun +4 1 +05 1997 Mar lastSun +4 e +04/+05 2004 Jun 27 +3 R +03/+04 2005 Mar lastSun 2 +4 - +04 +Z Asia/Dili 8:22:20 - LMT 1912 +8 - +08 1942 F 21 23 +9 - +09 1976 May 3 +8 - +08 2000 S 17 +9 - +09 +Z Asia/Kolkata 5:53:28 - LMT 1854 Jun 28 +5:53:20 - HMT 1870 +5:21:10 - MMT 1906 +5:30 - IST 1941 O +5:30 1 +0630 1942 May 15 +5:30 - IST 1942 S +5:30 1 +0630 1945 O 15 +5:30 - IST +Z Asia/Jakarta 7:7:12 - LMT 1867 Au 10 +7:7:12 - BMT 1923 D 31 23:47:12 +7:20 - +0720 1932 N +7:30 - +0730 1942 Mar 23 +9 - +09 1945 S 23 +7:30 - +0730 1948 May +8 - +08 1950 May +7:30 - +0730 1964 +7 - WIB +Z Asia/Pontianak 7:17:20 - LMT 1908 May +7:17:20 - PMT 1932 N +7:30 - +0730 1942 Ja 29 +9 - +09 1945 S 23 +7:30 - +0730 1948 May +8 - +08 1950 May +7:30 - +0730 1964 +8 - WITA 1988 +7 - WIB +Z Asia/Makassar 7:57:36 - LMT 1920 +7:57:36 - MMT 1932 N +8 - +08 1942 F 9 +9 - +09 1945 S 23 +8 - WITA +Z Asia/Jayapura 9:22:48 - LMT 1932 N +9 - +09 1944 S +9:30 - +0930 1964 +9 - WIT +R i 1978 1980 - Mar 21 0 1 - +R i 1978 o - O 21 0 0 - +R i 1979 o - S 19 0 0 - +R i 1980 o - S 23 0 0 - +R i 1991 o - May 3 0 1 - +R i 1992 1995 - Mar 22 0 1 - +R i 1991 1995 - S 22 0 0 - +R i 1996 o - Mar 21 0 1 - +R i 1996 o - S 21 0 0 - +R i 1997 1999 - Mar 22 0 1 - +R i 1997 1999 - S 22 0 0 - +R i 2000 o - Mar 21 0 1 - +R i 2000 o - S 21 0 0 - +R i 2001 2003 - Mar 22 0 1 - +R i 2001 2003 - S 22 0 0 - +R i 2004 o - Mar 21 0 1 - +R i 2004 o - S 21 0 0 - +R i 2005 o - Mar 22 0 1 - +R i 2005 o - S 22 0 0 - +R i 2008 o - Mar 21 0 1 - +R i 2008 o - S 21 0 0 - +R i 2009 2011 - Mar 22 0 1 - +R i 2009 2011 - S 22 0 0 - +R i 2012 o - Mar 21 0 1 - +R i 2012 o - S 21 0 0 - +R i 2013 2015 - Mar 22 0 1 - +R i 2013 2015 - S 22 0 0 - +R i 2016 o - Mar 21 0 1 - +R i 2016 o - S 21 0 0 - +R i 2017 2019 - Mar 22 0 1 - +R i 2017 2019 - S 22 0 0 - +R i 2020 o - Mar 21 0 1 - +R i 2020 o - S 21 0 0 - +R i 2021 2023 - Mar 22 0 1 - +R i 2021 2023 - S 22 0 0 - +R i 2024 o - Mar 21 0 1 - +R i 2024 o - S 21 0 0 - +R i 2025 2027 - Mar 22 0 1 - +R i 2025 2027 - S 22 0 0 - +R i 2028 2029 - Mar 21 0 1 - +R i 2028 2029 - S 21 0 0 - +R i 2030 2031 - Mar 22 0 1 - +R i 2030 2031 - S 22 0 0 - +R i 2032 2033 - Mar 21 0 1 - +R i 2032 2033 - S 21 0 0 - +R i 2034 2035 - Mar 22 0 1 - +R i 2034 2035 - S 22 0 0 - +R i 2036 ma - Mar 21 0 1 - +R i 2036 ma - S 21 0 0 - +Z Asia/Tehran 3:25:44 - LMT 1916 +3:25:44 - TMT 1946 +3:30 - +0330 1977 N +4 i +04/+05 1979 +3:30 i +0330/+0430 +R IQ 1982 o - May 1 0 1 - +R IQ 1982 1984 - O 1 0 0 - +R IQ 1983 o - Mar 31 0 1 - +R IQ 1984 1985 - Ap 1 0 1 - +R IQ 1985 1990 - S lastSun 1s 0 - +R IQ 1986 1990 - Mar lastSun 1s 1 - +R IQ 1991 2007 - Ap 1 3s 1 - +R IQ 1991 2007 - O 1 3s 0 - +Z Asia/Baghdad 2:57:40 - LMT 1890 +2:57:36 - BMT 1918 +3 - +03 1982 May +3 IQ +03/+04 +R Z 1940 o - Jun 1 0 1 D +R Z 1942 1944 - N 1 0 0 S +R Z 1943 o - Ap 1 2 1 D +R Z 1944 o - Ap 1 0 1 D +R Z 1945 o - Ap 16 0 1 D +R Z 1945 o - N 1 2 0 S +R Z 1946 o - Ap 16 2 1 D +R Z 1946 o - N 1 0 0 S +R Z 1948 o - May 23 0 2 DD +R Z 1948 o - S 1 0 1 D +R Z 1948 1949 - N 1 2 0 S +R Z 1949 o - May 1 0 1 D +R Z 1950 o - Ap 16 0 1 D +R Z 1950 o - S 15 3 0 S +R Z 1951 o - Ap 1 0 1 D +R Z 1951 o - N 11 3 0 S +R Z 1952 o - Ap 20 2 1 D +R Z 1952 o - O 19 3 0 S +R Z 1953 o - Ap 12 2 1 D +R Z 1953 o - S 13 3 0 S +R Z 1954 o - Jun 13 0 1 D +R Z 1954 o - S 12 0 0 S +R Z 1955 o - Jun 11 2 1 D +R Z 1955 o - S 11 0 0 S +R Z 1956 o - Jun 3 0 1 D +R Z 1956 o - S 30 3 0 S +R Z 1957 o - Ap 29 2 1 D +R Z 1957 o - S 22 0 0 S +R Z 1974 o - Jul 7 0 1 D +R Z 1974 o - O 13 0 0 S +R Z 1975 o - Ap 20 0 1 D +R Z 1975 o - Au 31 0 0 S +R Z 1985 o - Ap 14 0 1 D +R Z 1985 o - S 15 0 0 S +R Z 1986 o - May 18 0 1 D +R Z 1986 o - S 7 0 0 S +R Z 1987 o - Ap 15 0 1 D +R Z 1987 o - S 13 0 0 S +R Z 1988 o - Ap 10 0 1 D +R Z 1988 o - S 4 0 0 S +R Z 1989 o - Ap 30 0 1 D +R Z 1989 o - S 3 0 0 S +R Z 1990 o - Mar 25 0 1 D +R Z 1990 o - Au 26 0 0 S +R Z 1991 o - Mar 24 0 1 D +R Z 1991 o - S 1 0 0 S +R Z 1992 o - Mar 29 0 1 D +R Z 1992 o - S 6 0 0 S +R Z 1993 o - Ap 2 0 1 D +R Z 1993 o - S 5 0 0 S +R Z 1994 o - Ap 1 0 1 D +R Z 1994 o - Au 28 0 0 S +R Z 1995 o - Mar 31 0 1 D +R Z 1995 o - S 3 0 0 S +R Z 1996 o - Mar 15 0 1 D +R Z 1996 o - S 16 0 0 S +R Z 1997 o - Mar 21 0 1 D +R Z 1997 o - S 14 0 0 S +R Z 1998 o - Mar 20 0 1 D +R Z 1998 o - S 6 0 0 S +R Z 1999 o - Ap 2 2 1 D +R Z 1999 o - S 3 2 0 S +R Z 2000 o - Ap 14 2 1 D +R Z 2000 o - O 6 1 0 S +R Z 2001 o - Ap 9 1 1 D +R Z 2001 o - S 24 1 0 S +R Z 2002 o - Mar 29 1 1 D +R Z 2002 o - O 7 1 0 S +R Z 2003 o - Mar 28 1 1 D +R Z 2003 o - O 3 1 0 S +R Z 2004 o - Ap 7 1 1 D +R Z 2004 o - S 22 1 0 S +R Z 2005 o - Ap 1 2 1 D +R Z 2005 o - O 9 2 0 S +R Z 2006 2010 - Mar F>=26 2 1 D +R Z 2006 o - O 1 2 0 S +R Z 2007 o - S 16 2 0 S +R Z 2008 o - O 5 2 0 S +R Z 2009 o - S 27 2 0 S +R Z 2010 o - S 12 2 0 S +R Z 2011 o - Ap 1 2 1 D +R Z 2011 o - O 2 2 0 S +R Z 2012 o - Mar F>=26 2 1 D +R Z 2012 o - S 23 2 0 S +R Z 2013 ma - Mar F>=23 2 1 D +R Z 2013 ma - O lastSun 2 0 S +Z Asia/Jerusalem 2:20:54 - LMT 1880 +2:20:40 - JMT 1918 +2 Z I%sT +R JP 1948 o - May Sat>=1 24 1 D +R JP 1948 1951 - S Sat>=8 25 0 S +R JP 1949 o - Ap Sat>=1 24 1 D +R JP 1950 1951 - May Sat>=1 24 1 D +Z Asia/Tokyo 9:18:59 - LMT 1887 D 31 15u +9 JP J%sT +R J 1973 o - Jun 6 0 1 S +R J 1973 1975 - O 1 0 0 - +R J 1974 1977 - May 1 0 1 S +R J 1976 o - N 1 0 0 - +R J 1977 o - O 1 0 0 - +R J 1978 o - Ap 30 0 1 S +R J 1978 o - S 30 0 0 - +R J 1985 o - Ap 1 0 1 S +R J 1985 o - O 1 0 0 - +R J 1986 1988 - Ap F>=1 0 1 S +R J 1986 1990 - O F>=1 0 0 - +R J 1989 o - May 8 0 1 S +R J 1990 o - Ap 27 0 1 S +R J 1991 o - Ap 17 0 1 S +R J 1991 o - S 27 0 0 - +R J 1992 o - Ap 10 0 1 S +R J 1992 1993 - O F>=1 0 0 - +R J 1993 1998 - Ap F>=1 0 1 S +R J 1994 o - S F>=15 0 0 - +R J 1995 1998 - S F>=15 0s 0 - +R J 1999 o - Jul 1 0s 1 S +R J 1999 2002 - S lastF 0s 0 - +R J 2000 2001 - Mar lastTh 0s 1 S +R J 2002 2012 - Mar lastTh 24 1 S +R J 2003 o - O 24 0s 0 - +R J 2004 o - O 15 0s 0 - +R J 2005 o - S lastF 0s 0 - +R J 2006 2011 - O lastF 0s 0 - +R J 2013 o - D 20 0 0 - +R J 2014 ma - Mar lastTh 24 1 S +R J 2014 ma - O lastF 0s 0 - +Z Asia/Amman 2:23:44 - LMT 1931 +2 J EE%sT +Z Asia/Almaty 5:7:48 - LMT 1924 May 2 +5 - +05 1930 Jun 21 +6 R +06/+07 1991 Mar 31 2s +5 R +05/+06 1992 Ja 19 2s +6 R +06/+07 2004 O 31 2s +6 - +06 +Z Asia/Qyzylorda 4:21:52 - LMT 1924 May 2 +4 - +04 1930 Jun 21 +5 - +05 1981 Ap +5 1 +06 1981 O +6 - +06 1982 Ap +5 R +05/+06 1991 Mar 31 2s +4 R +04/+05 1991 S 29 2s +5 R +05/+06 1992 Ja 19 2s +6 R +06/+07 1992 Mar 29 2s +5 R +05/+06 2004 O 31 2s +6 - +06 +Z Asia/Aqtobe 3:48:40 - LMT 1924 May 2 +4 - +04 1930 Jun 21 +5 - +05 1981 Ap +5 1 +06 1981 O +6 - +06 1982 Ap +5 R +05/+06 1991 Mar 31 2s +4 R +04/+05 1992 Ja 19 2s +5 R +05/+06 2004 O 31 2s +5 - +05 +Z Asia/Aqtau 3:21:4 - LMT 1924 May 2 +4 - +04 1930 Jun 21 +5 - +05 1981 O +6 - +06 1982 Ap +5 R +05/+06 1991 Mar 31 2s +4 R +04/+05 1992 Ja 19 2s +5 R +05/+06 1994 S 25 2s +4 R +04/+05 2004 O 31 2s +5 - +05 +Z Asia/Atyrau 3:27:44 - LMT 1924 May 2 +3 - +03 1930 Jun 21 +5 - +05 1981 O +6 - +06 1982 Ap +5 R +05/+06 1991 Mar 31 2s +4 R +04/+05 1992 Ja 19 2s +5 R +05/+06 1999 Mar 28 2s +4 R +04/+05 2004 O 31 2s +5 - +05 +Z Asia/Oral 3:25:24 - LMT 1924 May 2 +3 - +03 1930 Jun 21 +5 - +05 1981 Ap +5 1 +06 1981 O +6 - +06 1982 Ap +5 R +05/+06 1989 Mar 26 2s +4 R +04/+05 1992 Ja 19 2s +5 R +05/+06 1992 Mar 29 2s +4 R +04/+05 2004 O 31 2s +5 - +05 +R KG 1992 1996 - Ap Sun>=7 0s 1 - +R KG 1992 1996 - S lastSun 0 0 - +R KG 1997 2005 - Mar lastSun 2:30 1 - +R KG 1997 2004 - O lastSun 2:30 0 - +Z Asia/Bishkek 4:58:24 - LMT 1924 May 2 +5 - +05 1930 Jun 21 +6 R +06/+07 1991 Mar 31 2s +5 R +05/+06 1991 Au 31 2 +5 KG +05/+06 2005 Au 12 +6 - +06 +R KR 1948 o - Jun 1 0 1 D +R KR 1948 o - S 13 0 0 S +R KR 1949 o - Ap 3 0 1 D +R KR 1949 1951 - S Sun>=8 0 0 S +R KR 1950 o - Ap 1 0 1 D +R KR 1951 o - May 6 0 1 D +R KR 1955 o - May 5 0 1 D +R KR 1955 o - S 9 0 0 S +R KR 1956 o - May 20 0 1 D +R KR 1956 o - S 30 0 0 S +R KR 1957 1960 - May Sun>=1 0 1 D +R KR 1957 1960 - S Sun>=18 0 0 S +R KR 1987 1988 - May Sun>=8 2 1 D +R KR 1987 1988 - O Sun>=8 3 0 S +Z Asia/Seoul 8:27:52 - LMT 1908 Ap +8:30 - KST 1912 +9 - JST 1945 S 8 +9 - KST 1954 Mar 21 +8:30 KR K%sT 1961 Au 10 +9 KR K%sT +Z Asia/Pyongyang 8:23 - LMT 1908 Ap +8:30 - KST 1912 +9 - JST 1945 Au 24 +9 - KST 2015 Au 15 +8:30 - KST 2018 May 4 23:30 +9 - KST +R l 1920 o - Mar 28 0 1 S +R l 1920 o - O 25 0 0 - +R l 1921 o - Ap 3 0 1 S +R l 1921 o - O 3 0 0 - +R l 1922 o - Mar 26 0 1 S +R l 1922 o - O 8 0 0 - +R l 1923 o - Ap 22 0 1 S +R l 1923 o - S 16 0 0 - +R l 1957 1961 - May 1 0 1 S +R l 1957 1961 - O 1 0 0 - +R l 1972 o - Jun 22 0 1 S +R l 1972 1977 - O 1 0 0 - +R l 1973 1977 - May 1 0 1 S +R l 1978 o - Ap 30 0 1 S +R l 1978 o - S 30 0 0 - +R l 1984 1987 - May 1 0 1 S +R l 1984 1991 - O 16 0 0 - +R l 1988 o - Jun 1 0 1 S +R l 1989 o - May 10 0 1 S +R l 1990 1992 - May 1 0 1 S +R l 1992 o - O 4 0 0 - +R l 1993 ma - Mar lastSun 0 1 S +R l 1993 1998 - S lastSun 0 0 - +R l 1999 ma - O lastSun 0 0 - +Z Asia/Beirut 2:22 - LMT 1880 +2 l EE%sT +R NB 1935 1941 - S 14 0 0:20 - +R NB 1935 1941 - D 14 0 0 - +Z Asia/Kuala_Lumpur 6:46:46 - LMT 1901 +6:55:25 - SMT 1905 Jun +7 - +07 1933 +7 0:20 +0720 1936 +7:20 - +0720 1941 S +7:30 - +0730 1942 F 16 +9 - +09 1945 S 12 +7:30 - +0730 1982 +8 - +08 +Z Asia/Kuching 7:21:20 - LMT 1926 Mar +7:30 - +0730 1933 +8 NB +08/+0820 1942 F 16 +9 - +09 1945 S 12 +8 - +08 +Z Indian/Maldives 4:54 - LMT 1880 +4:54 - MMT 1960 +5 - +05 +R X 1983 1984 - Ap 1 0 1 - +R X 1983 o - O 1 0 0 - +R X 1985 1998 - Mar lastSun 0 1 - +R X 1984 1998 - S lastSun 0 0 - +R X 2001 o - Ap lastSat 2 1 - +R X 2001 2006 - S lastSat 2 0 - +R X 2002 2006 - Mar lastSat 2 1 - +R X 2015 2016 - Mar lastSat 2 1 - +R X 2015 2016 - S lastSat 0 0 - +Z Asia/Hovd 6:6:36 - LMT 1905 Au +6 - +06 1978 +7 X +07/+08 +Z Asia/Ulaanbaatar 7:7:32 - LMT 1905 Au +7 - +07 1978 +8 X +08/+09 +Z Asia/Choibalsan 7:38 - LMT 1905 Au +7 - +07 1978 +8 - +08 1983 Ap +9 X +09/+10 2008 Mar 31 +8 X +08/+09 +Z Asia/Kathmandu 5:41:16 - LMT 1920 +5:30 - +0530 1986 +5:45 - +0545 +R PK 2002 o - Ap Sun>=2 0 1 S +R PK 2002 o - O Sun>=2 0 0 - +R PK 2008 o - Jun 1 0 1 S +R PK 2008 2009 - N 1 0 0 - +R PK 2009 o - Ap 15 0 1 S +Z Asia/Karachi 4:28:12 - LMT 1907 +5:30 - +0530 1942 S +5:30 1 +0630 1945 O 15 +5:30 - +0530 1951 S 30 +5 - +05 1971 Mar 26 +5 PK PK%sT +R P 1999 2005 - Ap F>=15 0 1 S +R P 1999 2003 - O F>=15 0 0 - +R P 2004 o - O 1 1 0 - +R P 2005 o - O 4 2 0 - +R P 2006 2007 - Ap 1 0 1 S +R P 2006 o - S 22 0 0 - +R P 2007 o - S Th>=8 2 0 - +R P 2008 2009 - Mar lastF 0 1 S +R P 2008 o - S 1 0 0 - +R P 2009 o - S F>=1 1 0 - +R P 2010 o - Mar 26 0 1 S +R P 2010 o - Au 11 0 0 - +R P 2011 o - Ap 1 0:1 1 S +R P 2011 o - Au 1 0 0 - +R P 2011 o - Au 30 0 1 S +R P 2011 o - S 30 0 0 - +R P 2012 2014 - Mar lastTh 24 1 S +R P 2012 o - S 21 1 0 - +R P 2013 o - S F>=21 0 0 - +R P 2014 2015 - O F>=21 0 0 - +R P 2015 o - Mar lastF 24 1 S +R P 2016 ma - Mar Sat>=22 1 1 S +R P 2016 ma - O lastSat 1 0 - +Z Asia/Gaza 2:17:52 - LMT 1900 O +2 Z EET/EEST 1948 May 15 +2 K EE%sT 1967 Jun 5 +2 Z I%sT 1996 +2 J EE%sT 1999 +2 P EE%sT 2008 Au 29 +2 - EET 2008 S +2 P EE%sT 2010 +2 - EET 2010 Mar 27 0:1 +2 P EE%sT 2011 Au +2 - EET 2012 +2 P EE%sT +Z Asia/Hebron 2:20:23 - LMT 1900 O +2 Z EET/EEST 1948 May 15 +2 K EE%sT 1967 Jun 5 +2 Z I%sT 1996 +2 J EE%sT 1999 +2 P EE%sT +R PH 1936 o - N 1 0 1 D +R PH 1937 o - F 1 0 0 S +R PH 1954 o - Ap 12 0 1 D +R PH 1954 o - Jul 1 0 0 S +R PH 1978 o - Mar 22 0 1 D +R PH 1978 o - S 21 0 0 S +Z Asia/Manila -15:56 - LMT 1844 D 31 +8:4 - LMT 1899 May 11 +8 PH P%sT 1942 May +9 - JST 1944 N +8 PH P%sT +Z Asia/Qatar 3:26:8 - LMT 1920 +4 - +04 1972 Jun +3 - +03 +Li Asia/Qatar Asia/Bahrain +Z Asia/Riyadh 3:6:52 - LMT 1947 Mar 14 +3 - +03 +Li Asia/Riyadh Asia/Aden +Li Asia/Riyadh Asia/Kuwait +Z Asia/Singapore 6:55:25 - LMT 1901 +6:55:25 - SMT 1905 Jun +7 - +07 1933 +7 0:20 +0720 1936 +7:20 - +0720 1941 S +7:30 - +0730 1942 F 16 +9 - +09 1945 S 12 +7:30 - +0730 1982 +8 - +08 +Z Asia/Colombo 5:19:24 - LMT 1880 +5:19:32 - MMT 1906 +5:30 - +0530 1942 Ja 5 +5:30 0:30 +06 1942 S +5:30 1 +0630 1945 O 16 2 +5:30 - +0530 1996 May 25 +6:30 - +0630 1996 O 26 0:30 +6 - +06 2006 Ap 15 0:30 +5:30 - +0530 +R S 1920 1923 - Ap Sun>=15 2 1 S +R S 1920 1923 - O Sun>=1 2 0 - +R S 1962 o - Ap 29 2 1 S +R S 1962 o - O 1 2 0 - +R S 1963 1965 - May 1 2 1 S +R S 1963 o - S 30 2 0 - +R S 1964 o - O 1 2 0 - +R S 1965 o - S 30 2 0 - +R S 1966 o - Ap 24 2 1 S +R S 1966 1976 - O 1 2 0 - +R S 1967 1978 - May 1 2 1 S +R S 1977 1978 - S 1 2 0 - +R S 1983 1984 - Ap 9 2 1 S +R S 1983 1984 - O 1 2 0 - +R S 1986 o - F 16 2 1 S +R S 1986 o - O 9 2 0 - +R S 1987 o - Mar 1 2 1 S +R S 1987 1988 - O 31 2 0 - +R S 1988 o - Mar 15 2 1 S +R S 1989 o - Mar 31 2 1 S +R S 1989 o - O 1 2 0 - +R S 1990 o - Ap 1 2 1 S +R S 1990 o - S 30 2 0 - +R S 1991 o - Ap 1 0 1 S +R S 1991 1992 - O 1 0 0 - +R S 1992 o - Ap 8 0 1 S +R S 1993 o - Mar 26 0 1 S +R S 1993 o - S 25 0 0 - +R S 1994 1996 - Ap 1 0 1 S +R S 1994 2005 - O 1 0 0 - +R S 1997 1998 - Mar lastM 0 1 S +R S 1999 2006 - Ap 1 0 1 S +R S 2006 o - S 22 0 0 - +R S 2007 o - Mar lastF 0 1 S +R S 2007 o - N F>=1 0 0 - +R S 2008 o - Ap F>=1 0 1 S +R S 2008 o - N 1 0 0 - +R S 2009 o - Mar lastF 0 1 S +R S 2010 2011 - Ap F>=1 0 1 S +R S 2012 ma - Mar lastF 0 1 S +R S 2009 ma - O lastF 0 0 - +Z Asia/Damascus 2:25:12 - LMT 1920 +2 S EE%sT +Z Asia/Dushanbe 4:35:12 - LMT 1924 May 2 +5 - +05 1930 Jun 21 +6 R +06/+07 1991 Mar 31 2s +5 1 +05/+06 1991 S 9 2s +5 - +05 +Z Asia/Bangkok 6:42:4 - LMT 1880 +6:42:4 - BMT 1920 Ap +7 - +07 +Li Asia/Bangkok Asia/Phnom_Penh +Li Asia/Bangkok Asia/Vientiane +Z Asia/Ashgabat 3:53:32 - LMT 1924 May 2 +4 - +04 1930 Jun 21 +5 R +05/+06 1991 Mar 31 2 +4 R +04/+05 1992 Ja 19 2 +5 - +05 +Z Asia/Dubai 3:41:12 - LMT 1920 +4 - +04 +Li Asia/Dubai Asia/Muscat +Z Asia/Samarkand 4:27:53 - LMT 1924 May 2 +4 - +04 1930 Jun 21 +5 - +05 1981 Ap +5 1 +06 1981 O +6 - +06 1982 Ap +5 R +05/+06 1992 +5 - +05 +Z Asia/Tashkent 4:37:11 - LMT 1924 May 2 +5 - +05 1930 Jun 21 +6 R +06/+07 1991 Mar 31 2 +5 R +05/+06 1992 +5 - +05 +Z Asia/Ho_Chi_Minh 7:6:40 - LMT 1906 Jul +7:6:30 - PLMT 1911 May +7 - +07 1942 D 31 23 +8 - +08 1945 Mar 14 23 +9 - +09 1945 S 2 +7 - +07 1947 Ap +8 - +08 1955 Jul +7 - +07 1959 D 31 23 +8 - +08 1975 Jun 13 +7 - +07 +R AU 1917 o - Ja 1 0:1 1 D +R AU 1917 o - Mar 25 2 0 S +R AU 1942 o - Ja 1 2 1 D +R AU 1942 o - Mar 29 2 0 S +R AU 1942 o - S 27 2 1 D +R AU 1943 1944 - Mar lastSun 2 0 S +R AU 1943 o - O 3 2 1 D +Z Australia/Darwin 8:43:20 - LMT 1895 F +9 - ACST 1899 May +9:30 AU AC%sT +R AW 1974 o - O lastSun 2s 1 D +R AW 1975 o - Mar Sun>=1 2s 0 S +R AW 1983 o - O lastSun 2s 1 D +R AW 1984 o - Mar Sun>=1 2s 0 S +R AW 1991 o - N 17 2s 1 D +R AW 1992 o - Mar Sun>=1 2s 0 S +R AW 2006 o - D 3 2s 1 D +R AW 2007 2009 - Mar lastSun 2s 0 S +R AW 2007 2008 - O lastSun 2s 1 D +Z Australia/Perth 7:43:24 - LMT 1895 D +8 AU AW%sT 1943 Jul +8 AW AW%sT +Z Australia/Eucla 8:35:28 - LMT 1895 D +8:45 AU +0845/+0945 1943 Jul +8:45 AW +0845/+0945 +R AQ 1971 o - O lastSun 2s 1 D +R AQ 1972 o - F lastSun 2s 0 S +R AQ 1989 1991 - O lastSun 2s 1 D +R AQ 1990 1992 - Mar Sun>=1 2s 0 S +R Ho 1992 1993 - O lastSun 2s 1 D +R Ho 1993 1994 - Mar Sun>=1 2s 0 S +Z Australia/Brisbane 10:12:8 - LMT 1895 +10 AU AE%sT 1971 +10 AQ AE%sT +Z Australia/Lindeman 9:55:56 - LMT 1895 +10 AU AE%sT 1971 +10 AQ AE%sT 1992 Jul +10 Ho AE%sT +R AS 1971 1985 - O lastSun 2s 1 D +R AS 1986 o - O 19 2s 1 D +R AS 1987 2007 - O lastSun 2s 1 D +R AS 1972 o - F 27 2s 0 S +R AS 1973 1985 - Mar Sun>=1 2s 0 S +R AS 1986 1990 - Mar Sun>=15 2s 0 S +R AS 1991 o - Mar 3 2s 0 S +R AS 1992 o - Mar 22 2s 0 S +R AS 1993 o - Mar 7 2s 0 S +R AS 1994 o - Mar 20 2s 0 S +R AS 1995 2005 - Mar lastSun 2s 0 S +R AS 2006 o - Ap 2 2s 0 S +R AS 2007 o - Mar lastSun 2s 0 S +R AS 2008 ma - Ap Sun>=1 2s 0 S +R AS 2008 ma - O Sun>=1 2s 1 D +Z Australia/Adelaide 9:14:20 - LMT 1895 F +9 - ACST 1899 May +9:30 AU AC%sT 1971 +9:30 AS AC%sT +R AT 1967 o - O Sun>=1 2s 1 D +R AT 1968 o - Mar lastSun 2s 0 S +R AT 1968 1985 - O lastSun 2s 1 D +R AT 1969 1971 - Mar Sun>=8 2s 0 S +R AT 1972 o - F lastSun 2s 0 S +R AT 1973 1981 - Mar Sun>=1 2s 0 S +R AT 1982 1983 - Mar lastSun 2s 0 S +R AT 1984 1986 - Mar Sun>=1 2s 0 S +R AT 1986 o - O Sun>=15 2s 1 D +R AT 1987 1990 - Mar Sun>=15 2s 0 S +R AT 1987 o - O Sun>=22 2s 1 D +R AT 1988 1990 - O lastSun 2s 1 D +R AT 1991 1999 - O Sun>=1 2s 1 D +R AT 1991 2005 - Mar lastSun 2s 0 S +R AT 2000 o - Au lastSun 2s 1 D +R AT 2001 ma - O Sun>=1 2s 1 D +R AT 2006 o - Ap Sun>=1 2s 0 S +R AT 2007 o - Mar lastSun 2s 0 S +R AT 2008 ma - Ap Sun>=1 2s 0 S +Z Australia/Hobart 9:49:16 - LMT 1895 S +10 - AEST 1916 O 1 2 +10 1 AEDT 1917 F +10 AU AE%sT 1967 +10 AT AE%sT +Z Australia/Currie 9:35:28 - LMT 1895 S +10 - AEST 1916 O 1 2 +10 1 AEDT 1917 F +10 AU AE%sT 1971 Jul +10 AT AE%sT +R AV 1971 1985 - O lastSun 2s 1 D +R AV 1972 o - F lastSun 2s 0 S +R AV 1973 1985 - Mar Sun>=1 2s 0 S +R AV 1986 1990 - Mar Sun>=15 2s 0 S +R AV 1986 1987 - O Sun>=15 2s 1 D +R AV 1988 1999 - O lastSun 2s 1 D +R AV 1991 1994 - Mar Sun>=1 2s 0 S +R AV 1995 2005 - Mar lastSun 2s 0 S +R AV 2000 o - Au lastSun 2s 1 D +R AV 2001 2007 - O lastSun 2s 1 D +R AV 2006 o - Ap Sun>=1 2s 0 S +R AV 2007 o - Mar lastSun 2s 0 S +R AV 2008 ma - Ap Sun>=1 2s 0 S +R AV 2008 ma - O Sun>=1 2s 1 D +Z Australia/Melbourne 9:39:52 - LMT 1895 F +10 AU AE%sT 1971 +10 AV AE%sT +R AN 1971 1985 - O lastSun 2s 1 D +R AN 1972 o - F 27 2s 0 S +R AN 1973 1981 - Mar Sun>=1 2s 0 S +R AN 1982 o - Ap Sun>=1 2s 0 S +R AN 1983 1985 - Mar Sun>=1 2s 0 S +R AN 1986 1989 - Mar Sun>=15 2s 0 S +R AN 1986 o - O 19 2s 1 D +R AN 1987 1999 - O lastSun 2s 1 D +R AN 1990 1995 - Mar Sun>=1 2s 0 S +R AN 1996 2005 - Mar lastSun 2s 0 S +R AN 2000 o - Au lastSun 2s 1 D +R AN 2001 2007 - O lastSun 2s 1 D +R AN 2006 o - Ap Sun>=1 2s 0 S +R AN 2007 o - Mar lastSun 2s 0 S +R AN 2008 ma - Ap Sun>=1 2s 0 S +R AN 2008 ma - O Sun>=1 2s 1 D +Z Australia/Sydney 10:4:52 - LMT 1895 F +10 AU AE%sT 1971 +10 AN AE%sT +Z Australia/Broken_Hill 9:25:48 - LMT 1895 F +10 - AEST 1896 Au 23 +9 - ACST 1899 May +9:30 AU AC%sT 1971 +9:30 AN AC%sT 2000 +9:30 AS AC%sT +R LH 1981 1984 - O lastSun 2 1 - +R LH 1982 1985 - Mar Sun>=1 2 0 - +R LH 1985 o - O lastSun 2 0:30 - +R LH 1986 1989 - Mar Sun>=15 2 0 - +R LH 1986 o - O 19 2 0:30 - +R LH 1987 1999 - O lastSun 2 0:30 - +R LH 1990 1995 - Mar Sun>=1 2 0 - +R LH 1996 2005 - Mar lastSun 2 0 - +R LH 2000 o - Au lastSun 2 0:30 - +R LH 2001 2007 - O lastSun 2 0:30 - +R LH 2006 o - Ap Sun>=1 2 0 - +R LH 2007 o - Mar lastSun 2 0 - +R LH 2008 ma - Ap Sun>=1 2 0 - +R LH 2008 ma - O Sun>=1 2 0:30 - +Z Australia/Lord_Howe 10:36:20 - LMT 1895 F +10 - AEST 1981 Mar +10:30 LH +1030/+1130 1985 Jul +10:30 LH +1030/+11 +Z Antarctica/Macquarie 0 - -00 1899 N +10 - AEST 1916 O 1 2 +10 1 AEDT 1917 F +10 AU AE%sT 1919 Ap 1 0s +0 - -00 1948 Mar 25 +10 AU AE%sT 1967 +10 AT AE%sT 2010 Ap 4 3 +11 - +11 +Z Indian/Christmas 7:2:52 - LMT 1895 F +7 - +07 +Z Indian/Cocos 6:27:40 - LMT 1900 +6:30 - +0630 +R FJ 1998 1999 - N Sun>=1 2 1 - +R FJ 1999 2000 - F lastSun 3 0 - +R FJ 2009 o - N 29 2 1 - +R FJ 2010 o - Mar lastSun 3 0 - +R FJ 2010 2013 - O Sun>=21 2 1 - +R FJ 2011 o - Mar Sun>=1 3 0 - +R FJ 2012 2013 - Ja Sun>=18 3 0 - +R FJ 2014 o - Ja Sun>=18 2 0 - +R FJ 2014 ma - N Sun>=1 2 1 - +R FJ 2015 ma - Ja Sun>=13 3 0 - +Z Pacific/Fiji 11:55:44 - LMT 1915 O 26 +12 FJ +12/+13 +Z Pacific/Gambier -8:59:48 - LMT 1912 O +-9 - -09 +Z Pacific/Marquesas -9:18 - LMT 1912 O +-9:30 - -0930 +Z Pacific/Tahiti -9:58:16 - LMT 1912 O +-10 - -10 +Z Pacific/Guam -14:21 - LMT 1844 D 31 +9:39 - LMT 1901 +10 - GST 2000 D 23 +10 - ChST +Li Pacific/Guam Pacific/Saipan +Z Pacific/Tarawa 11:32:4 - LMT 1901 +12 - +12 +Z Pacific/Enderbury -11:24:20 - LMT 1901 +-12 - -12 1979 O +-11 - -11 1994 D 31 +13 - +13 +Z Pacific/Kiritimati -10:29:20 - LMT 1901 +-10:40 - -1040 1979 O +-10 - -10 1994 D 31 +14 - +14 +Z Pacific/Majuro 11:24:48 - LMT 1901 +11 - +11 1969 O +12 - +12 +Z Pacific/Kwajalein 11:9:20 - LMT 1901 +11 - +11 1969 O +-12 - -12 1993 Au 20 +12 - +12 +Z Pacific/Chuuk 10:7:8 - LMT 1901 +10 - +10 +Z Pacific/Pohnpei 10:32:52 - LMT 1901 +11 - +11 +Z Pacific/Kosrae 10:51:56 - LMT 1901 +11 - +11 1969 O +12 - +12 1999 +11 - +11 +Z Pacific/Nauru 11:7:40 - LMT 1921 Ja 15 +11:30 - +1130 1942 Mar 15 +9 - +09 1944 Au 15 +11:30 - +1130 1979 May +12 - +12 +R NC 1977 1978 - D Sun>=1 0 1 - +R NC 1978 1979 - F 27 0 0 - +R NC 1996 o - D 1 2s 1 - +R NC 1997 o - Mar 2 2s 0 - +Z Pacific/Noumea 11:5:48 - LMT 1912 Ja 13 +11 NC +11/+12 +R NZ 1927 o - N 6 2 1 S +R NZ 1928 o - Mar 4 2 0 M +R NZ 1928 1933 - O Sun>=8 2 0:30 S +R NZ 1929 1933 - Mar Sun>=15 2 0 M +R NZ 1934 1940 - Ap lastSun 2 0 M +R NZ 1934 1940 - S lastSun 2 0:30 S +R NZ 1946 o - Ja 1 0 0 S +R NZ 1974 o - N Sun>=1 2s 1 D +R k 1974 o - N Sun>=1 2:45s 1 - +R NZ 1975 o - F lastSun 2s 0 S +R k 1975 o - F lastSun 2:45s 0 - +R NZ 1975 1988 - O lastSun 2s 1 D +R k 1975 1988 - O lastSun 2:45s 1 - +R NZ 1976 1989 - Mar Sun>=1 2s 0 S +R k 1976 1989 - Mar Sun>=1 2:45s 0 - +R NZ 1989 o - O Sun>=8 2s 1 D +R k 1989 o - O Sun>=8 2:45s 1 - +R NZ 1990 2006 - O Sun>=1 2s 1 D +R k 1990 2006 - O Sun>=1 2:45s 1 - +R NZ 1990 2007 - Mar Sun>=15 2s 0 S +R k 1990 2007 - Mar Sun>=15 2:45s 0 - +R NZ 2007 ma - S lastSun 2s 1 D +R k 2007 ma - S lastSun 2:45s 1 - +R NZ 2008 ma - Ap Sun>=1 2s 0 S +R k 2008 ma - Ap Sun>=1 2:45s 0 - +Z Pacific/Auckland 11:39:4 - LMT 1868 N 2 +11:30 NZ NZ%sT 1946 +12 NZ NZ%sT +Z Pacific/Chatham 12:13:48 - LMT 1868 N 2 +12:15 - +1215 1946 +12:45 k +1245/+1345 +Li Pacific/Auckland Antarctica/McMurdo +R CK 1978 o - N 12 0 0:30 - +R CK 1979 1991 - Mar Sun>=1 0 0 - +R CK 1979 1990 - O lastSun 0 0:30 - +Z Pacific/Rarotonga -10:39:4 - LMT 1901 +-10:30 - -1030 1978 N 12 +-10 CK -10/-0930 +Z Pacific/Niue -11:19:40 - LMT 1901 +-11:20 - -1120 1951 +-11:30 - -1130 1978 O +-11 - -11 +Z Pacific/Norfolk 11:11:52 - LMT 1901 +11:12 - +1112 1951 +11:30 - +1130 1974 O 27 2 +11:30 1 +1230 1975 Mar 2 2 +11:30 - +1130 2015 O 4 2 +11 - +11 +Z Pacific/Palau 8:57:56 - LMT 1901 +9 - +09 +Z Pacific/Port_Moresby 9:48:40 - LMT 1880 +9:48:32 - PMMT 1895 +10 - +10 +Z Pacific/Bougainville 10:22:16 - LMT 1880 +9:48:32 - PMMT 1895 +10 - +10 1942 Jul +9 - +09 1945 Au 21 +10 - +10 2014 D 28 2 +11 - +11 +Z Pacific/Pitcairn -8:40:20 - LMT 1901 +-8:30 - -0830 1998 Ap 27 +-8 - -08 +Z Pacific/Pago_Pago 12:37:12 - LMT 1892 Jul 5 +-11:22:48 - LMT 1911 +-11 - SST +Li Pacific/Pago_Pago Pacific/Midway +R WS 2010 o - S lastSun 0 1 - +R WS 2011 o - Ap Sat>=1 4 0 - +R WS 2011 o - S lastSat 3 1 - +R WS 2012 ma - Ap Sun>=1 4 0 - +R WS 2012 ma - S lastSun 3 1 - +Z Pacific/Apia 12:33:4 - LMT 1892 Jul 5 +-11:26:56 - LMT 1911 +-11:30 - -1130 1950 +-11 WS -11/-10 2011 D 29 24 +13 WS +13/+14 +Z Pacific/Guadalcanal 10:39:48 - LMT 1912 O +11 - +11 +Z Pacific/Fakaofo -11:24:56 - LMT 1901 +-11 - -11 2011 D 30 +13 - +13 +R TO 1999 o - O 7 2s 1 - +R TO 2000 o - Mar 19 2s 0 - +R TO 2000 2001 - N Sun>=1 2 1 - +R TO 2001 2002 - Ja lastSun 2 0 - +R TO 2016 o - N Sun>=1 2 1 - +R TO 2017 o - Ja Sun>=15 3 0 - +Z Pacific/Tongatapu 12:19:20 - LMT 1901 +12:20 - +1220 1941 +13 - +13 1999 +13 TO +13/+14 +Z Pacific/Funafuti 11:56:52 - LMT 1901 +12 - +12 +Z Pacific/Wake 11:6:28 - LMT 1901 +12 - +12 +R VU 1983 o - S 25 0 1 - +R VU 1984 1991 - Mar Sun>=23 0 0 - +R VU 1984 o - O 23 0 1 - +R VU 1985 1991 - S Sun>=23 0 1 - +R VU 1992 1993 - Ja Sun>=23 0 0 - +R VU 1992 o - O Sun>=23 0 1 - +Z Pacific/Efate 11:13:16 - LMT 1912 Ja 13 +11 VU +11/+12 +Z Pacific/Wallis 12:15:20 - LMT 1901 +12 - +12 +R G 1916 o - May 21 2s 1 BST +R G 1916 o - O 1 2s 0 GMT +R G 1917 o - Ap 8 2s 1 BST +R G 1917 o - S 17 2s 0 GMT +R G 1918 o - Mar 24 2s 1 BST +R G 1918 o - S 30 2s 0 GMT +R G 1919 o - Mar 30 2s 1 BST +R G 1919 o - S 29 2s 0 GMT +R G 1920 o - Mar 28 2s 1 BST +R G 1920 o - O 25 2s 0 GMT +R G 1921 o - Ap 3 2s 1 BST +R G 1921 o - O 3 2s 0 GMT +R G 1922 o - Mar 26 2s 1 BST +R G 1922 o - O 8 2s 0 GMT +R G 1923 o - Ap Sun>=16 2s 1 BST +R G 1923 1924 - S Sun>=16 2s 0 GMT +R G 1924 o - Ap Sun>=9 2s 1 BST +R G 1925 1926 - Ap Sun>=16 2s 1 BST +R G 1925 1938 - O Sun>=2 2s 0 GMT +R G 1927 o - Ap Sun>=9 2s 1 BST +R G 1928 1929 - Ap Sun>=16 2s 1 BST +R G 1930 o - Ap Sun>=9 2s 1 BST +R G 1931 1932 - Ap Sun>=16 2s 1 BST +R G 1933 o - Ap Sun>=9 2s 1 BST +R G 1934 o - Ap Sun>=16 2s 1 BST +R G 1935 o - Ap Sun>=9 2s 1 BST +R G 1936 1937 - Ap Sun>=16 2s 1 BST +R G 1938 o - Ap Sun>=9 2s 1 BST +R G 1939 o - Ap Sun>=16 2s 1 BST +R G 1939 o - N Sun>=16 2s 0 GMT +R G 1940 o - F Sun>=23 2s 1 BST +R G 1941 o - May Sun>=2 1s 2 BDST +R G 1941 1943 - Au Sun>=9 1s 1 BST +R G 1942 1944 - Ap Sun>=2 1s 2 BDST +R G 1944 o - S Sun>=16 1s 1 BST +R G 1945 o - Ap M>=2 1s 2 BDST +R G 1945 o - Jul Sun>=9 1s 1 BST +R G 1945 1946 - O Sun>=2 2s 0 GMT +R G 1946 o - Ap Sun>=9 2s 1 BST +R G 1947 o - Mar 16 2s 1 BST +R G 1947 o - Ap 13 1s 2 BDST +R G 1947 o - Au 10 1s 1 BST +R G 1947 o - N 2 2s 0 GMT +R G 1948 o - Mar 14 2s 1 BST +R G 1948 o - O 31 2s 0 GMT +R G 1949 o - Ap 3 2s 1 BST +R G 1949 o - O 30 2s 0 GMT +R G 1950 1952 - Ap Sun>=14 2s 1 BST +R G 1950 1952 - O Sun>=21 2s 0 GMT +R G 1953 o - Ap Sun>=16 2s 1 BST +R G 1953 1960 - O Sun>=2 2s 0 GMT +R G 1954 o - Ap Sun>=9 2s 1 BST +R G 1955 1956 - Ap Sun>=16 2s 1 BST +R G 1957 o - Ap Sun>=9 2s 1 BST +R G 1958 1959 - Ap Sun>=16 2s 1 BST +R G 1960 o - Ap Sun>=9 2s 1 BST +R G 1961 1963 - Mar lastSun 2s 1 BST +R G 1961 1968 - O Sun>=23 2s 0 GMT +R G 1964 1967 - Mar Sun>=19 2s 1 BST +R G 1968 o - F 18 2s 1 BST +R G 1972 1980 - Mar Sun>=16 2s 1 BST +R G 1972 1980 - O Sun>=23 2s 0 GMT +R G 1981 1995 - Mar lastSun 1u 1 BST +R G 1981 1989 - O Sun>=23 1u 0 GMT +R G 1990 1995 - O Sun>=22 1u 0 GMT +Z Europe/London -0:1:15 - LMT 1847 D 1 0s +0 G %s 1968 O 27 +1 - BST 1971 O 31 2u +0 G %s 1996 +0 E GMT/BST +Li Europe/London Europe/Jersey +Li Europe/London Europe/Guernsey +Li Europe/London Europe/Isle_of_Man +R IE 1971 o - O 31 2u -1 - +R IE 1972 1980 - Mar Sun>=16 2u 0 - +R IE 1972 1980 - O Sun>=23 2u -1 - +R IE 1981 ma - Mar lastSun 1u 0 - +R IE 1981 1989 - O Sun>=23 1u -1 - +R IE 1990 1995 - O Sun>=22 1u -1 - +R IE 1996 ma - O lastSun 1u -1 - +Z Europe/Dublin -0:25 - LMT 1880 Au 2 +-0:25:21 - DMT 1916 May 21 2s +-0:25:21 1 IST 1916 O 1 2s +0 G %s 1921 D 6 +0 G GMT/IST 1940 F 25 2s +0 1 IST 1946 O 6 2s +0 - GMT 1947 Mar 16 2s +0 1 IST 1947 N 2 2s +0 - GMT 1948 Ap 18 2s +0 G GMT/IST 1968 O 27 +1 IE IST/GMT +R E 1977 1980 - Ap Sun>=1 1u 1 S +R E 1977 o - S lastSun 1u 0 - +R E 1978 o - O 1 1u 0 - +R E 1979 1995 - S lastSun 1u 0 - +R E 1981 ma - Mar lastSun 1u 1 S +R E 1996 ma - O lastSun 1u 0 - +R W- 1977 1980 - Ap Sun>=1 1s 1 S +R W- 1977 o - S lastSun 1s 0 - +R W- 1978 o - O 1 1s 0 - +R W- 1979 1995 - S lastSun 1s 0 - +R W- 1981 ma - Mar lastSun 1s 1 S +R W- 1996 ma - O lastSun 1s 0 - +R c 1916 o - Ap 30 23 1 S +R c 1916 o - O 1 1 0 - +R c 1917 1918 - Ap M>=15 2s 1 S +R c 1917 1918 - S M>=15 2s 0 - +R c 1940 o - Ap 1 2s 1 S +R c 1942 o - N 2 2s 0 - +R c 1943 o - Mar 29 2s 1 S +R c 1943 o - O 4 2s 0 - +R c 1944 1945 - Ap M>=1 2s 1 S +R c 1944 o - O 2 2s 0 - +R c 1945 o - S 16 2s 0 - +R c 1977 1980 - Ap Sun>=1 2s 1 S +R c 1977 o - S lastSun 2s 0 - +R c 1978 o - O 1 2s 0 - +R c 1979 1995 - S lastSun 2s 0 - +R c 1981 ma - Mar lastSun 2s 1 S +R c 1996 ma - O lastSun 2s 0 - +R e 1977 1980 - Ap Sun>=1 0 1 S +R e 1977 o - S lastSun 0 0 - +R e 1978 o - O 1 0 0 - +R e 1979 1995 - S lastSun 0 0 - +R e 1981 ma - Mar lastSun 0 1 S +R e 1996 ma - O lastSun 0 0 - +R R 1917 o - Jul 1 23 1 MST +R R 1917 o - D 28 0 0 MMT +R R 1918 o - May 31 22 2 MDST +R R 1918 o - S 16 1 1 MST +R R 1919 o - May 31 23 2 MDST +R R 1919 o - Jul 1 0u 1 MSD +R R 1919 o - Au 16 0 0 MSK +R R 1921 o - F 14 23 1 MSD +R R 1921 o - Mar 20 23 2 +05 +R R 1921 o - S 1 0 1 MSD +R R 1921 o - O 1 0 0 - +R R 1981 1984 - Ap 1 0 1 S +R R 1981 1983 - O 1 0 0 - +R R 1984 1995 - S lastSun 2s 0 - +R R 1985 2010 - Mar lastSun 2s 1 S +R R 1996 2010 - O lastSun 2s 0 - +Z WET 0 E WE%sT +Z CET 1 c CE%sT +Z MET 1 c ME%sT +Z EET 2 E EE%sT +R q 1940 o - Jun 16 0 1 S +R q 1942 o - N 2 3 0 - +R q 1943 o - Mar 29 2 1 S +R q 1943 o - Ap 10 3 0 - +R q 1974 o - May 4 0 1 S +R q 1974 o - O 2 0 0 - +R q 1975 o - May 1 0 1 S +R q 1975 o - O 2 0 0 - +R q 1976 o - May 2 0 1 S +R q 1976 o - O 3 0 0 - +R q 1977 o - May 8 0 1 S +R q 1977 o - O 2 0 0 - +R q 1978 o - May 6 0 1 S +R q 1978 o - O 1 0 0 - +R q 1979 o - May 5 0 1 S +R q 1979 o - S 30 0 0 - +R q 1980 o - May 3 0 1 S +R q 1980 o - O 4 0 0 - +R q 1981 o - Ap 26 0 1 S +R q 1981 o - S 27 0 0 - +R q 1982 o - May 2 0 1 S +R q 1982 o - O 3 0 0 - +R q 1983 o - Ap 18 0 1 S +R q 1983 o - O 1 0 0 - +R q 1984 o - Ap 1 0 1 S +Z Europe/Tirane 1:19:20 - LMT 1914 +1 - CET 1940 Jun 16 +1 q CE%sT 1984 Jul +1 E CE%sT +Z Europe/Andorra 0:6:4 - LMT 1901 +0 - WET 1946 S 30 +1 - CET 1985 Mar 31 2 +1 E CE%sT +R a 1920 o - Ap 5 2s 1 S +R a 1920 o - S 13 2s 0 - +R a 1946 o - Ap 14 2s 1 S +R a 1946 1948 - O Sun>=1 2s 0 - +R a 1947 o - Ap 6 2s 1 S +R a 1948 o - Ap 18 2s 1 S +R a 1980 o - Ap 6 0 1 S +R a 1980 o - S 28 0 0 - +Z Europe/Vienna 1:5:21 - LMT 1893 Ap +1 c CE%sT 1920 +1 a CE%sT 1940 Ap 1 2s +1 c CE%sT 1945 Ap 2 2s +1 1 CEST 1945 Ap 12 2s +1 - CET 1946 +1 a CE%sT 1981 +1 E CE%sT +Z Europe/Minsk 1:50:16 - LMT 1880 +1:50 - MMT 1924 May 2 +2 - EET 1930 Jun 21 +3 - MSK 1941 Jun 28 +1 c CE%sT 1944 Jul 3 +3 R MSK/MSD 1990 +3 - MSK 1991 Mar 31 2s +2 R EE%sT 2011 Mar 27 2s +3 - +03 +R b 1918 o - Mar 9 0s 1 S +R b 1918 1919 - O Sat>=1 23s 0 - +R b 1919 o - Mar 1 23s 1 S +R b 1920 o - F 14 23s 1 S +R b 1920 o - O 23 23s 0 - +R b 1921 o - Mar 14 23s 1 S +R b 1921 o - O 25 23s 0 - +R b 1922 o - Mar 25 23s 1 S +R b 1922 1927 - O Sat>=1 23s 0 - +R b 1923 o - Ap 21 23s 1 S +R b 1924 o - Mar 29 23s 1 S +R b 1925 o - Ap 4 23s 1 S +R b 1926 o - Ap 17 23s 1 S +R b 1927 o - Ap 9 23s 1 S +R b 1928 o - Ap 14 23s 1 S +R b 1928 1938 - O Sun>=2 2s 0 - +R b 1929 o - Ap 21 2s 1 S +R b 1930 o - Ap 13 2s 1 S +R b 1931 o - Ap 19 2s 1 S +R b 1932 o - Ap 3 2s 1 S +R b 1933 o - Mar 26 2s 1 S +R b 1934 o - Ap 8 2s 1 S +R b 1935 o - Mar 31 2s 1 S +R b 1936 o - Ap 19 2s 1 S +R b 1937 o - Ap 4 2s 1 S +R b 1938 o - Mar 27 2s 1 S +R b 1939 o - Ap 16 2s 1 S +R b 1939 o - N 19 2s 0 - +R b 1940 o - F 25 2s 1 S +R b 1944 o - S 17 2s 0 - +R b 1945 o - Ap 2 2s 1 S +R b 1945 o - S 16 2s 0 - +R b 1946 o - May 19 2s 1 S +R b 1946 o - O 7 2s 0 - +Z Europe/Brussels 0:17:30 - LMT 1880 +0:17:30 - BMT 1892 May 1 12 +0 - WET 1914 N 8 +1 - CET 1916 May +1 c CE%sT 1918 N 11 11u +0 b WE%sT 1940 May 20 2s +1 c CE%sT 1944 S 3 +1 b CE%sT 1977 +1 E CE%sT +R BG 1979 o - Mar 31 23 1 S +R BG 1979 o - O 1 1 0 - +R BG 1980 1982 - Ap Sat>=1 23 1 S +R BG 1980 o - S 29 1 0 - +R BG 1981 o - S 27 2 0 - +Z Europe/Sofia 1:33:16 - LMT 1880 +1:56:56 - IMT 1894 N 30 +2 - EET 1942 N 2 3 +1 c CE%sT 1945 +1 - CET 1945 Ap 2 3 +2 - EET 1979 Mar 31 23 +2 BG EE%sT 1982 S 26 3 +2 c EE%sT 1991 +2 e EE%sT 1997 +2 E EE%sT +R CZ 1945 o - Ap M>=1 2s 1 S +R CZ 1945 o - O 1 2s 0 - +R CZ 1946 o - May 6 2s 1 S +R CZ 1946 1949 - O Sun>=1 2s 0 - +R CZ 1947 1948 - Ap Sun>=15 2s 1 S +R CZ 1949 o - Ap 9 2s 1 S +Z Europe/Prague 0:57:44 - LMT 1850 +0:57:44 - PMT 1891 O +1 c CE%sT 1945 May 9 +1 CZ CE%sT 1946 D 1 3 +1 -1 GMT 1947 F 23 2 +1 CZ CE%sT 1979 +1 E CE%sT +R D 1916 o - May 14 23 1 S +R D 1916 o - S 30 23 0 - +R D 1940 o - May 15 0 1 S +R D 1945 o - Ap 2 2s 1 S +R D 1945 o - Au 15 2s 0 - +R D 1946 o - May 1 2s 1 S +R D 1946 o - S 1 2s 0 - +R D 1947 o - May 4 2s 1 S +R D 1947 o - Au 10 2s 0 - +R D 1948 o - May 9 2s 1 S +R D 1948 o - Au 8 2s 0 - +Z Europe/Copenhagen 0:50:20 - LMT 1890 +0:50:20 - CMT 1894 +1 D CE%sT 1942 N 2 2s +1 c CE%sT 1945 Ap 2 2 +1 D CE%sT 1980 +1 E CE%sT +Z Atlantic/Faroe -0:27:4 - LMT 1908 Ja 11 +0 - WET 1981 +0 E WE%sT +R Th 1991 1992 - Mar lastSun 2 1 D +R Th 1991 1992 - S lastSun 2 0 S +R Th 1993 2006 - Ap Sun>=1 2 1 D +R Th 1993 2006 - O lastSun 2 0 S +R Th 2007 ma - Mar Sun>=8 2 1 D +R Th 2007 ma - N Sun>=1 2 0 S +Z America/Danmarkshavn -1:14:40 - LMT 1916 Jul 28 +-3 - -03 1980 Ap 6 2 +-3 E -03/-02 1996 +0 - GMT +Z America/Scoresbysund -1:27:52 - LMT 1916 Jul 28 +-2 - -02 1980 Ap 6 2 +-2 c -02/-01 1981 Mar 29 +-1 E -01/+00 +Z America/Godthab -3:26:56 - LMT 1916 Jul 28 +-3 - -03 1980 Ap 6 2 +-3 E -03/-02 +Z America/Thule -4:35:8 - LMT 1916 Jul 28 +-4 Th A%sT +Z Europe/Tallinn 1:39 - LMT 1880 +1:39 - TMT 1918 F +1 c CE%sT 1919 Jul +1:39 - TMT 1921 May +2 - EET 1940 Au 6 +3 - MSK 1941 S 15 +1 c CE%sT 1944 S 22 +3 R MSK/MSD 1989 Mar 26 2s +2 1 EEST 1989 S 24 2s +2 c EE%sT 1998 S 22 +2 E EE%sT 1999 O 31 4 +2 - EET 2002 F 21 +2 E EE%sT +R FI 1942 o - Ap 2 24 1 S +R FI 1942 o - O 4 1 0 - +R FI 1981 1982 - Mar lastSun 2 1 S +R FI 1981 1982 - S lastSun 3 0 - +Z Europe/Helsinki 1:39:49 - LMT 1878 May 31 +1:39:49 - HMT 1921 May +2 FI EE%sT 1983 +2 E EE%sT +Li Europe/Helsinki Europe/Mariehamn +R F 1916 o - Jun 14 23s 1 S +R F 1916 1919 - O Sun>=1 23s 0 - +R F 1917 o - Mar 24 23s 1 S +R F 1918 o - Mar 9 23s 1 S +R F 1919 o - Mar 1 23s 1 S +R F 1920 o - F 14 23s 1 S +R F 1920 o - O 23 23s 0 - +R F 1921 o - Mar 14 23s 1 S +R F 1921 o - O 25 23s 0 - +R F 1922 o - Mar 25 23s 1 S +R F 1922 1938 - O Sat>=1 23s 0 - +R F 1923 o - May 26 23s 1 S +R F 1924 o - Mar 29 23s 1 S +R F 1925 o - Ap 4 23s 1 S +R F 1926 o - Ap 17 23s 1 S +R F 1927 o - Ap 9 23s 1 S +R F 1928 o - Ap 14 23s 1 S +R F 1929 o - Ap 20 23s 1 S +R F 1930 o - Ap 12 23s 1 S +R F 1931 o - Ap 18 23s 1 S +R F 1932 o - Ap 2 23s 1 S +R F 1933 o - Mar 25 23s 1 S +R F 1934 o - Ap 7 23s 1 S +R F 1935 o - Mar 30 23s 1 S +R F 1936 o - Ap 18 23s 1 S +R F 1937 o - Ap 3 23s 1 S +R F 1938 o - Mar 26 23s 1 S +R F 1939 o - Ap 15 23s 1 S +R F 1939 o - N 18 23s 0 - +R F 1940 o - F 25 2 1 S +R F 1941 o - May 5 0 2 M +R F 1941 o - O 6 0 1 S +R F 1942 o - Mar 9 0 2 M +R F 1942 o - N 2 3 1 S +R F 1943 o - Mar 29 2 2 M +R F 1943 o - O 4 3 1 S +R F 1944 o - Ap 3 2 2 M +R F 1944 o - O 8 1 1 S +R F 1945 o - Ap 2 2 2 M +R F 1945 o - S 16 3 0 - +R F 1976 o - Mar 28 1 1 S +R F 1976 o - S 26 1 0 - +Z Europe/Paris 0:9:21 - LMT 1891 Mar 15 0:1 +0:9:21 - PMT 1911 Mar 11 0:1 +0 F WE%sT 1940 Jun 14 23 +1 c CE%sT 1944 Au 25 +0 F WE%sT 1945 S 16 3 +1 F CE%sT 1977 +1 E CE%sT +R DE 1946 o - Ap 14 2s 1 S +R DE 1946 o - O 7 2s 0 - +R DE 1947 1949 - O Sun>=1 2s 0 - +R DE 1947 o - Ap 6 3s 1 S +R DE 1947 o - May 11 2s 2 M +R DE 1947 o - Jun 29 3 1 S +R DE 1948 o - Ap 18 2s 1 S +R DE 1949 o - Ap 10 2s 1 S +R So 1945 o - May 24 2 2 M +R So 1945 o - S 24 3 1 S +R So 1945 o - N 18 2s 0 - +Z Europe/Berlin 0:53:28 - LMT 1893 Ap +1 c CE%sT 1945 May 24 2 +1 So CE%sT 1946 +1 DE CE%sT 1980 +1 E CE%sT +Li Europe/Zurich Europe/Busingen +Z Europe/Gibraltar -0:21:24 - LMT 1880 Au 2 0s +0 G %s 1957 Ap 14 2 +1 - CET 1982 +1 E CE%sT +R g 1932 o - Jul 7 0 1 S +R g 1932 o - S 1 0 0 - +R g 1941 o - Ap 7 0 1 S +R g 1942 o - N 2 3 0 - +R g 1943 o - Mar 30 0 1 S +R g 1943 o - O 4 0 0 - +R g 1952 o - Jul 1 0 1 S +R g 1952 o - N 2 0 0 - +R g 1975 o - Ap 12 0s 1 S +R g 1975 o - N 26 0s 0 - +R g 1976 o - Ap 11 2s 1 S +R g 1976 o - O 10 2s 0 - +R g 1977 1978 - Ap Sun>=1 2s 1 S +R g 1977 o - S 26 2s 0 - +R g 1978 o - S 24 4 0 - +R g 1979 o - Ap 1 9 1 S +R g 1979 o - S 29 2 0 - +R g 1980 o - Ap 1 0 1 S +R g 1980 o - S 28 0 0 - +Z Europe/Athens 1:34:52 - LMT 1895 S 14 +1:34:52 - AMT 1916 Jul 28 0:1 +2 g EE%sT 1941 Ap 30 +1 g CE%sT 1944 Ap 4 +2 g EE%sT 1981 +2 E EE%sT +R h 1918 o - Ap 1 3 1 S +R h 1918 o - S 16 3 0 - +R h 1919 o - Ap 15 3 1 S +R h 1919 o - N 24 3 0 - +R h 1945 o - May 1 23 1 S +R h 1945 o - N 1 0 0 - +R h 1946 o - Mar 31 2s 1 S +R h 1946 1949 - O Sun>=1 2s 0 - +R h 1947 1949 - Ap Sun>=4 2s 1 S +R h 1950 o - Ap 17 2s 1 S +R h 1950 o - O 23 2s 0 - +R h 1954 1955 - May 23 0 1 S +R h 1954 1955 - O 3 0 0 - +R h 1956 o - Jun Sun>=1 0 1 S +R h 1956 o - S lastSun 0 0 - +R h 1957 o - Jun Sun>=1 1 1 S +R h 1957 o - S lastSun 3 0 - +R h 1980 o - Ap 6 1 1 S +Z Europe/Budapest 1:16:20 - LMT 1890 O +1 c CE%sT 1918 +1 h CE%sT 1941 Ap 8 +1 c CE%sT 1945 +1 h CE%sT 1980 S 28 2s +1 E CE%sT +R w 1917 1919 - F 19 23 1 - +R w 1917 o - O 21 1 0 - +R w 1918 1919 - N 16 1 0 - +R w 1921 o - Mar 19 23 1 - +R w 1921 o - Jun 23 1 0 - +R w 1939 o - Ap 29 23 1 - +R w 1939 o - O 29 2 0 - +R w 1940 o - F 25 2 1 - +R w 1940 1941 - N Sun>=2 1s 0 - +R w 1941 1942 - Mar Sun>=2 1s 1 - +R w 1943 1946 - Mar Sun>=1 1s 1 - +R w 1942 1948 - O Sun>=22 1s 0 - +R w 1947 1967 - Ap Sun>=1 1s 1 - +R w 1949 o - O 30 1s 0 - +R w 1950 1966 - O Sun>=22 1s 0 - +R w 1967 o - O 29 1s 0 - +Z Atlantic/Reykjavik -1:28 - LMT 1908 +-1 w -01/+00 1968 Ap 7 1s +0 - GMT +R I 1916 o - Jun 3 24 1 S +R I 1916 1917 - S 30 24 0 - +R I 1917 o - Mar 31 24 1 S +R I 1918 o - Mar 9 24 1 S +R I 1918 o - O 6 24 0 - +R I 1919 o - Mar 1 24 1 S +R I 1919 o - O 4 24 0 - +R I 1920 o - Mar 20 24 1 S +R I 1920 o - S 18 24 0 - +R I 1940 o - Jun 14 24 1 S +R I 1942 o - N 2 2s 0 - +R I 1943 o - Mar 29 2s 1 S +R I 1943 o - O 4 2s 0 - +R I 1944 o - Ap 2 2s 1 S +R I 1944 o - S 17 2s 0 - +R I 1945 o - Ap 2 2 1 S +R I 1945 o - S 15 1 0 - +R I 1946 o - Mar 17 2s 1 S +R I 1946 o - O 6 2s 0 - +R I 1947 o - Mar 16 0s 1 S +R I 1947 o - O 5 0s 0 - +R I 1948 o - F 29 2s 1 S +R I 1948 o - O 3 2s 0 - +R I 1966 1968 - May Sun>=22 0s 1 S +R I 1966 o - S 24 24 0 - +R I 1967 1969 - S Sun>=22 0s 0 - +R I 1969 o - Jun 1 0s 1 S +R I 1970 o - May 31 0s 1 S +R I 1970 o - S lastSun 0s 0 - +R I 1971 1972 - May Sun>=22 0s 1 S +R I 1971 o - S lastSun 0s 0 - +R I 1972 o - O 1 0s 0 - +R I 1973 o - Jun 3 0s 1 S +R I 1973 1974 - S lastSun 0s 0 - +R I 1974 o - May 26 0s 1 S +R I 1975 o - Jun 1 0s 1 S +R I 1975 1977 - S lastSun 0s 0 - +R I 1976 o - May 30 0s 1 S +R I 1977 1979 - May Sun>=22 0s 1 S +R I 1978 o - O 1 0s 0 - +R I 1979 o - S 30 0s 0 - +Z Europe/Rome 0:49:56 - LMT 1866 S 22 +0:49:56 - RMT 1893 O 31 23:49:56 +1 I CE%sT 1943 S 10 +1 c CE%sT 1944 Jun 4 +1 I CE%sT 1980 +1 E CE%sT +Li Europe/Rome Europe/Vatican +Li Europe/Rome Europe/San_Marino +R LV 1989 1996 - Mar lastSun 2s 1 S +R LV 1989 1996 - S lastSun 2s 0 - +Z Europe/Riga 1:36:34 - LMT 1880 +1:36:34 - RMT 1918 Ap 15 2 +1:36:34 1 LST 1918 S 16 3 +1:36:34 - RMT 1919 Ap 1 2 +1:36:34 1 LST 1919 May 22 3 +1:36:34 - RMT 1926 May 11 +2 - EET 1940 Au 5 +3 - MSK 1941 Jul +1 c CE%sT 1944 O 13 +3 R MSK/MSD 1989 Mar lastSun 2s +2 1 EEST 1989 S lastSun 2s +2 LV EE%sT 1997 Ja 21 +2 E EE%sT 2000 F 29 +2 - EET 2001 Ja 2 +2 E EE%sT +Li Europe/Zurich Europe/Vaduz +Z Europe/Vilnius 1:41:16 - LMT 1880 +1:24 - WMT 1917 +1:35:36 - KMT 1919 O 10 +1 - CET 1920 Jul 12 +2 - EET 1920 O 9 +1 - CET 1940 Au 3 +3 - MSK 1941 Jun 24 +1 c CE%sT 1944 Au +3 R MSK/MSD 1989 Mar 26 2s +2 R EE%sT 1991 S 29 2s +2 c EE%sT 1998 +2 - EET 1998 Mar 29 1u +1 E CE%sT 1999 O 31 1u +2 - EET 2003 +2 E EE%sT +R LX 1916 o - May 14 23 1 S +R LX 1916 o - O 1 1 0 - +R LX 1917 o - Ap 28 23 1 S +R LX 1917 o - S 17 1 0 - +R LX 1918 o - Ap M>=15 2s 1 S +R LX 1918 o - S M>=15 2s 0 - +R LX 1919 o - Mar 1 23 1 S +R LX 1919 o - O 5 3 0 - +R LX 1920 o - F 14 23 1 S +R LX 1920 o - O 24 2 0 - +R LX 1921 o - Mar 14 23 1 S +R LX 1921 o - O 26 2 0 - +R LX 1922 o - Mar 25 23 1 S +R LX 1922 o - O Sun>=2 1 0 - +R LX 1923 o - Ap 21 23 1 S +R LX 1923 o - O Sun>=2 2 0 - +R LX 1924 o - Mar 29 23 1 S +R LX 1924 1928 - O Sun>=2 1 0 - +R LX 1925 o - Ap 5 23 1 S +R LX 1926 o - Ap 17 23 1 S +R LX 1927 o - Ap 9 23 1 S +R LX 1928 o - Ap 14 23 1 S +R LX 1929 o - Ap 20 23 1 S +Z Europe/Luxembourg 0:24:36 - LMT 1904 Jun +1 LX CE%sT 1918 N 25 +0 LX WE%sT 1929 O 6 2s +0 b WE%sT 1940 May 14 3 +1 c WE%sT 1944 S 18 3 +1 b CE%sT 1977 +1 E CE%sT +R MT 1973 o - Mar 31 0s 1 S +R MT 1973 o - S 29 0s 0 - +R MT 1974 o - Ap 21 0s 1 S +R MT 1974 o - S 16 0s 0 - +R MT 1975 1979 - Ap Sun>=15 2 1 S +R MT 1975 1980 - S Sun>=15 2 0 - +R MT 1980 o - Mar 31 2 1 S +Z Europe/Malta 0:58:4 - LMT 1893 N 2 0s +1 I CE%sT 1973 Mar 31 +1 MT CE%sT 1981 +1 E CE%sT +R MD 1997 ma - Mar lastSun 2 1 S +R MD 1997 ma - O lastSun 3 0 - +Z Europe/Chisinau 1:55:20 - LMT 1880 +1:55 - CMT 1918 F 15 +1:44:24 - BMT 1931 Jul 24 +2 z EE%sT 1940 Au 15 +2 1 EEST 1941 Jul 17 +1 c CE%sT 1944 Au 24 +3 R MSK/MSD 1990 May 6 2 +2 R EE%sT 1992 +2 e EE%sT 1997 +2 MD EE%sT +Z Europe/Monaco 0:29:32 - LMT 1891 Mar 15 +0:9:21 - PMT 1911 Mar 11 +0 F WE%sT 1945 S 16 3 +1 F CE%sT 1977 +1 E CE%sT +R N 1916 o - May 1 0 1 NST +R N 1916 o - O 1 0 0 AMT +R N 1917 o - Ap 16 2s 1 NST +R N 1917 o - S 17 2s 0 AMT +R N 1918 1921 - Ap M>=1 2s 1 NST +R N 1918 1921 - S lastM 2s 0 AMT +R N 1922 o - Mar lastSun 2s 1 NST +R N 1922 1936 - O Sun>=2 2s 0 AMT +R N 1923 o - Jun F>=1 2s 1 NST +R N 1924 o - Mar lastSun 2s 1 NST +R N 1925 o - Jun F>=1 2s 1 NST +R N 1926 1931 - May 15 2s 1 NST +R N 1932 o - May 22 2s 1 NST +R N 1933 1936 - May 15 2s 1 NST +R N 1937 o - May 22 2s 1 NST +R N 1937 o - Jul 1 0 1 S +R N 1937 1939 - O Sun>=2 2s 0 - +R N 1938 1939 - May 15 2s 1 S +R N 1945 o - Ap 2 2s 1 S +R N 1945 o - S 16 2s 0 - +Z Europe/Amsterdam 0:19:32 - LMT 1835 +0:19:32 N %s 1937 Jul +0:20 N +0020/+0120 1940 May 16 +1 c CE%sT 1945 Ap 2 2 +1 N CE%sT 1977 +1 E CE%sT +R NO 1916 o - May 22 1 1 S +R NO 1916 o - S 30 0 0 - +R NO 1945 o - Ap 2 2s 1 S +R NO 1945 o - O 1 2s 0 - +R NO 1959 1964 - Mar Sun>=15 2s 1 S +R NO 1959 1965 - S Sun>=15 2s 0 - +R NO 1965 o - Ap 25 2s 1 S +Z Europe/Oslo 0:43 - LMT 1895 +1 NO CE%sT 1940 Au 10 23 +1 c CE%sT 1945 Ap 2 2 +1 NO CE%sT 1980 +1 E CE%sT +Li Europe/Oslo Arctic/Longyearbyen +R O 1918 1919 - S 16 2s 0 - +R O 1919 o - Ap 15 2s 1 S +R O 1944 o - Ap 3 2s 1 S +R O 1944 o - O 4 2 0 - +R O 1945 o - Ap 29 0 1 S +R O 1945 o - N 1 0 0 - +R O 1946 o - Ap 14 0s 1 S +R O 1946 o - O 7 2s 0 - +R O 1947 o - May 4 2s 1 S +R O 1947 1949 - O Sun>=1 2s 0 - +R O 1948 o - Ap 18 2s 1 S +R O 1949 o - Ap 10 2s 1 S +R O 1957 o - Jun 2 1s 1 S +R O 1957 1958 - S lastSun 1s 0 - +R O 1958 o - Mar 30 1s 1 S +R O 1959 o - May 31 1s 1 S +R O 1959 1961 - O Sun>=1 1s 0 - +R O 1960 o - Ap 3 1s 1 S +R O 1961 1964 - May lastSun 1s 1 S +R O 1962 1964 - S lastSun 1s 0 - +Z Europe/Warsaw 1:24 - LMT 1880 +1:24 - WMT 1915 Au 5 +1 c CE%sT 1918 S 16 3 +2 O EE%sT 1922 Jun +1 O CE%sT 1940 Jun 23 2 +1 c CE%sT 1944 O +1 O CE%sT 1977 +1 W- CE%sT 1988 +1 E CE%sT +R p 1916 o - Jun 17 23 1 S +R p 1916 o - N 1 1 0 - +R p 1917 o - F 28 23s 1 S +R p 1917 1921 - O 14 23s 0 - +R p 1918 o - Mar 1 23s 1 S +R p 1919 o - F 28 23s 1 S +R p 1920 o - F 29 23s 1 S +R p 1921 o - F 28 23s 1 S +R p 1924 o - Ap 16 23s 1 S +R p 1924 o - O 14 23s 0 - +R p 1926 o - Ap 17 23s 1 S +R p 1926 1929 - O Sat>=1 23s 0 - +R p 1927 o - Ap 9 23s 1 S +R p 1928 o - Ap 14 23s 1 S +R p 1929 o - Ap 20 23s 1 S +R p 1931 o - Ap 18 23s 1 S +R p 1931 1932 - O Sat>=1 23s 0 - +R p 1932 o - Ap 2 23s 1 S +R p 1934 o - Ap 7 23s 1 S +R p 1934 1938 - O Sat>=1 23s 0 - +R p 1935 o - Mar 30 23s 1 S +R p 1936 o - Ap 18 23s 1 S +R p 1937 o - Ap 3 23s 1 S +R p 1938 o - Mar 26 23s 1 S +R p 1939 o - Ap 15 23s 1 S +R p 1939 o - N 18 23s 0 - +R p 1940 o - F 24 23s 1 S +R p 1940 1941 - O 5 23s 0 - +R p 1941 o - Ap 5 23s 1 S +R p 1942 1945 - Mar Sat>=8 23s 1 S +R p 1942 o - Ap 25 22s 2 M +R p 1942 o - Au 15 22s 1 S +R p 1942 1945 - O Sat>=24 23s 0 - +R p 1943 o - Ap 17 22s 2 M +R p 1943 1945 - Au Sat>=25 22s 1 S +R p 1944 1945 - Ap Sat>=21 22s 2 M +R p 1946 o - Ap Sat>=1 23s 1 S +R p 1946 o - O Sat>=1 23s 0 - +R p 1947 1949 - Ap Sun>=1 2s 1 S +R p 1947 1949 - O Sun>=1 2s 0 - +R p 1951 1965 - Ap Sun>=1 2s 1 S +R p 1951 1965 - O Sun>=1 2s 0 - +R p 1977 o - Mar 27 0s 1 S +R p 1977 o - S 25 0s 0 - +R p 1978 1979 - Ap Sun>=1 0s 1 S +R p 1978 o - O 1 0s 0 - +R p 1979 1982 - S lastSun 1s 0 - +R p 1980 o - Mar lastSun 0s 1 S +R p 1981 1982 - Mar lastSun 1s 1 S +R p 1983 o - Mar lastSun 2s 1 S +Z Europe/Lisbon -0:36:45 - LMT 1884 +-0:36:45 - LMT 1912 Ja 1 0u +0 p WE%sT 1966 Ap 3 2 +1 - CET 1976 S 26 1 +0 p WE%sT 1983 S 25 1s +0 W- WE%sT 1992 S 27 1s +1 E CE%sT 1996 Mar 31 1u +0 E WE%sT +Z Atlantic/Azores -1:42:40 - LMT 1884 +-1:54:32 - HMT 1912 Ja 1 2u +-2 p -02/-01 1942 Ap 25 22s +-2 p +00 1942 Au 15 22s +-2 p -02/-01 1943 Ap 17 22s +-2 p +00 1943 Au 28 22s +-2 p -02/-01 1944 Ap 22 22s +-2 p +00 1944 Au 26 22s +-2 p -02/-01 1945 Ap 21 22s +-2 p +00 1945 Au 25 22s +-2 p -02/-01 1966 Ap 3 2 +-1 p -01/+00 1983 S 25 1s +-1 W- -01/+00 1992 S 27 1s +0 E WE%sT 1993 Mar 28 1u +-1 E -01/+00 +Z Atlantic/Madeira -1:7:36 - LMT 1884 +-1:7:36 - FMT 1912 Ja 1 1u +-1 p -01/+00 1942 Ap 25 22s +-1 p +01 1942 Au 15 22s +-1 p -01/+00 1943 Ap 17 22s +-1 p +01 1943 Au 28 22s +-1 p -01/+00 1944 Ap 22 22s +-1 p +01 1944 Au 26 22s +-1 p -01/+00 1945 Ap 21 22s +-1 p +01 1945 Au 25 22s +-1 p -01/+00 1966 Ap 3 2 +0 p WE%sT 1983 S 25 1s +0 E WE%sT +R z 1932 o - May 21 0s 1 S +R z 1932 1939 - O Sun>=1 0s 0 - +R z 1933 1939 - Ap Sun>=2 0s 1 S +R z 1979 o - May 27 0 1 S +R z 1979 o - S lastSun 0 0 - +R z 1980 o - Ap 5 23 1 S +R z 1980 o - S lastSun 1 0 - +R z 1991 1993 - Mar lastSun 0s 1 S +R z 1991 1993 - S lastSun 0s 0 - +Z Europe/Bucharest 1:44:24 - LMT 1891 O +1:44:24 - BMT 1931 Jul 24 +2 z EE%sT 1981 Mar 29 2s +2 c EE%sT 1991 +2 z EE%sT 1994 +2 e EE%sT 1997 +2 E EE%sT +Z Europe/Kaliningrad 1:22 - LMT 1893 Ap +1 c CE%sT 1945 +2 O CE%sT 1946 +3 R MSK/MSD 1989 Mar 26 2s +2 R EE%sT 2011 Mar 27 2s +3 - +03 2014 O 26 2s +2 - EET +Z Europe/Moscow 2:30:17 - LMT 1880 +2:30:17 - MMT 1916 Jul 3 +2:31:19 R %s 1919 Jul 1 0u +3 R %s 1921 O +3 R MSK/MSD 1922 O +2 - EET 1930 Jun 21 +3 R MSK/MSD 1991 Mar 31 2s +2 R EE%sT 1992 Ja 19 2s +3 R MSK/MSD 2011 Mar 27 2s +4 - MSK 2014 O 26 2s +3 - MSK +Z Europe/Simferopol 2:16:24 - LMT 1880 +2:16 - SMT 1924 May 2 +2 - EET 1930 Jun 21 +3 - MSK 1941 N +1 c CE%sT 1944 Ap 13 +3 R MSK/MSD 1990 +3 - MSK 1990 Jul 1 2 +2 - EET 1992 +2 e EE%sT 1994 May +3 e MSK/MSD 1996 Mar 31 0s +3 1 MSD 1996 O 27 3s +3 R MSK/MSD 1997 +3 - MSK 1997 Mar lastSun 1u +2 E EE%sT 2014 Mar 30 2 +4 - MSK 2014 O 26 2s +3 - MSK +Z Europe/Astrakhan 3:12:12 - LMT 1924 May +3 - +03 1930 Jun 21 +4 R +04/+05 1989 Mar 26 2s +3 R +03/+04 1991 Mar 31 2s +4 - +04 1992 Mar 29 2s +3 R +03/+04 2011 Mar 27 2s +4 - +04 2014 O 26 2s +3 - +03 2016 Mar 27 2s +4 - +04 +Z Europe/Volgograd 2:57:40 - LMT 1920 Ja 3 +3 - +03 1930 Jun 21 +4 - +04 1961 N 11 +4 R +04/+05 1988 Mar 27 2s +3 R +03/+04 1991 Mar 31 2s +4 - +04 1992 Mar 29 2s +3 R +03/+04 2011 Mar 27 2s +4 - +04 2014 O 26 2s +3 - +03 2018 O 28 2s +4 - +04 +Z Europe/Saratov 3:4:18 - LMT 1919 Jul 1 0u +3 - +03 1930 Jun 21 +4 R +04/+05 1988 Mar 27 2s +3 R +03/+04 1991 Mar 31 2s +4 - +04 1992 Mar 29 2s +3 R +03/+04 2011 Mar 27 2s +4 - +04 2014 O 26 2s +3 - +03 2016 D 4 2s +4 - +04 +Z Europe/Kirov 3:18:48 - LMT 1919 Jul 1 0u +3 - +03 1930 Jun 21 +4 R +04/+05 1989 Mar 26 2s +3 R +03/+04 1991 Mar 31 2s +4 - +04 1992 Mar 29 2s +3 R +03/+04 2011 Mar 27 2s +4 - +04 2014 O 26 2s +3 - +03 +Z Europe/Samara 3:20:20 - LMT 1919 Jul 1 0u +3 - +03 1930 Jun 21 +4 - +04 1935 Ja 27 +4 R +04/+05 1989 Mar 26 2s +3 R +03/+04 1991 Mar 31 2s +2 R +02/+03 1991 S 29 2s +3 - +03 1991 O 20 3 +4 R +04/+05 2010 Mar 28 2s +3 R +03/+04 2011 Mar 27 2s +4 - +04 +Z Europe/Ulyanovsk 3:13:36 - LMT 1919 Jul 1 0u +3 - +03 1930 Jun 21 +4 R +04/+05 1989 Mar 26 2s +3 R +03/+04 1991 Mar 31 2s +2 R +02/+03 1992 Ja 19 2s +3 R +03/+04 2011 Mar 27 2s +4 - +04 2014 O 26 2s +3 - +03 2016 Mar 27 2s +4 - +04 +Z Asia/Yekaterinburg 4:2:33 - LMT 1916 Jul 3 +3:45:5 - PMT 1919 Jul 15 4 +4 - +04 1930 Jun 21 +5 R +05/+06 1991 Mar 31 2s +4 R +04/+05 1992 Ja 19 2s +5 R +05/+06 2011 Mar 27 2s +6 - +06 2014 O 26 2s +5 - +05 +Z Asia/Omsk 4:53:30 - LMT 1919 N 14 +5 - +05 1930 Jun 21 +6 R +06/+07 1991 Mar 31 2s +5 R +05/+06 1992 Ja 19 2s +6 R +06/+07 2011 Mar 27 2s +7 - +07 2014 O 26 2s +6 - +06 +Z Asia/Barnaul 5:35 - LMT 1919 D 10 +6 - +06 1930 Jun 21 +7 R +07/+08 1991 Mar 31 2s +6 R +06/+07 1992 Ja 19 2s +7 R +07/+08 1995 May 28 +6 R +06/+07 2011 Mar 27 2s +7 - +07 2014 O 26 2s +6 - +06 2016 Mar 27 2s +7 - +07 +Z Asia/Novosibirsk 5:31:40 - LMT 1919 D 14 6 +6 - +06 1930 Jun 21 +7 R +07/+08 1991 Mar 31 2s +6 R +06/+07 1992 Ja 19 2s +7 R +07/+08 1993 May 23 +6 R +06/+07 2011 Mar 27 2s +7 - +07 2014 O 26 2s +6 - +06 2016 Jul 24 2s +7 - +07 +Z Asia/Tomsk 5:39:51 - LMT 1919 D 22 +6 - +06 1930 Jun 21 +7 R +07/+08 1991 Mar 31 2s +6 R +06/+07 1992 Ja 19 2s +7 R +07/+08 2002 May 1 3 +6 R +06/+07 2011 Mar 27 2s +7 - +07 2014 O 26 2s +6 - +06 2016 May 29 2s +7 - +07 +Z Asia/Novokuznetsk 5:48:48 - LMT 1924 May +6 - +06 1930 Jun 21 +7 R +07/+08 1991 Mar 31 2s +6 R +06/+07 1992 Ja 19 2s +7 R +07/+08 2010 Mar 28 2s +6 R +06/+07 2011 Mar 27 2s +7 - +07 +Z Asia/Krasnoyarsk 6:11:26 - LMT 1920 Ja 6 +6 - +06 1930 Jun 21 +7 R +07/+08 1991 Mar 31 2s +6 R +06/+07 1992 Ja 19 2s +7 R +07/+08 2011 Mar 27 2s +8 - +08 2014 O 26 2s +7 - +07 +Z Asia/Irkutsk 6:57:5 - LMT 1880 +6:57:5 - IMT 1920 Ja 25 +7 - +07 1930 Jun 21 +8 R +08/+09 1991 Mar 31 2s +7 R +07/+08 1992 Ja 19 2s +8 R +08/+09 2011 Mar 27 2s +9 - +09 2014 O 26 2s +8 - +08 +Z Asia/Chita 7:33:52 - LMT 1919 D 15 +8 - +08 1930 Jun 21 +9 R +09/+10 1991 Mar 31 2s +8 R +08/+09 1992 Ja 19 2s +9 R +09/+10 2011 Mar 27 2s +10 - +10 2014 O 26 2s +8 - +08 2016 Mar 27 2 +9 - +09 +Z Asia/Yakutsk 8:38:58 - LMT 1919 D 15 +8 - +08 1930 Jun 21 +9 R +09/+10 1991 Mar 31 2s +8 R +08/+09 1992 Ja 19 2s +9 R +09/+10 2011 Mar 27 2s +10 - +10 2014 O 26 2s +9 - +09 +Z Asia/Vladivostok 8:47:31 - LMT 1922 N 15 +9 - +09 1930 Jun 21 +10 R +10/+11 1991 Mar 31 2s +9 R +09/+10 1992 Ja 19 2s +10 R +10/+11 2011 Mar 27 2s +11 - +11 2014 O 26 2s +10 - +10 +Z Asia/Khandyga 9:2:13 - LMT 1919 D 15 +8 - +08 1930 Jun 21 +9 R +09/+10 1991 Mar 31 2s +8 R +08/+09 1992 Ja 19 2s +9 R +09/+10 2004 +10 R +10/+11 2011 Mar 27 2s +11 - +11 2011 S 13 0s +10 - +10 2014 O 26 2s +9 - +09 +Z Asia/Sakhalin 9:30:48 - LMT 1905 Au 23 +9 - +09 1945 Au 25 +11 R +11/+12 1991 Mar 31 2s +10 R +10/+11 1992 Ja 19 2s +11 R +11/+12 1997 Mar lastSun 2s +10 R +10/+11 2011 Mar 27 2s +11 - +11 2014 O 26 2s +10 - +10 2016 Mar 27 2s +11 - +11 +Z Asia/Magadan 10:3:12 - LMT 1924 May 2 +10 - +10 1930 Jun 21 +11 R +11/+12 1991 Mar 31 2s +10 R +10/+11 1992 Ja 19 2s +11 R +11/+12 2011 Mar 27 2s +12 - +12 2014 O 26 2s +10 - +10 2016 Ap 24 2s +11 - +11 +Z Asia/Srednekolymsk 10:14:52 - LMT 1924 May 2 +10 - +10 1930 Jun 21 +11 R +11/+12 1991 Mar 31 2s +10 R +10/+11 1992 Ja 19 2s +11 R +11/+12 2011 Mar 27 2s +12 - +12 2014 O 26 2s +11 - +11 +Z Asia/Ust-Nera 9:32:54 - LMT 1919 D 15 +8 - +08 1930 Jun 21 +9 R +09/+10 1981 Ap +11 R +11/+12 1991 Mar 31 2s +10 R +10/+11 1992 Ja 19 2s +11 R +11/+12 2011 Mar 27 2s +12 - +12 2011 S 13 0s +11 - +11 2014 O 26 2s +10 - +10 +Z Asia/Kamchatka 10:34:36 - LMT 1922 N 10 +11 - +11 1930 Jun 21 +12 R +12/+13 1991 Mar 31 2s +11 R +11/+12 1992 Ja 19 2s +12 R +12/+13 2010 Mar 28 2s +11 R +11/+12 2011 Mar 27 2s +12 - +12 +Z Asia/Anadyr 11:49:56 - LMT 1924 May 2 +12 - +12 1930 Jun 21 +13 R +13/+14 1982 Ap 1 0s +12 R +12/+13 1991 Mar 31 2s +11 R +11/+12 1992 Ja 19 2s +12 R +12/+13 2010 Mar 28 2s +11 R +11/+12 2011 Mar 27 2s +12 - +12 +Z Europe/Belgrade 1:22 - LMT 1884 +1 - CET 1941 Ap 18 23 +1 c CE%sT 1945 +1 - CET 1945 May 8 2s +1 1 CEST 1945 S 16 2s +1 - CET 1982 N 27 +1 E CE%sT +Li Europe/Belgrade Europe/Ljubljana +Li Europe/Belgrade Europe/Podgorica +Li Europe/Belgrade Europe/Sarajevo +Li Europe/Belgrade Europe/Skopje +Li Europe/Belgrade Europe/Zagreb +Li Europe/Prague Europe/Bratislava +R s 1918 o - Ap 15 23 1 S +R s 1918 1919 - O 6 24s 0 - +R s 1919 o - Ap 6 23 1 S +R s 1924 o - Ap 16 23 1 S +R s 1924 o - O 4 24s 0 - +R s 1926 o - Ap 17 23 1 S +R s 1926 1929 - O Sat>=1 24s 0 - +R s 1927 o - Ap 9 23 1 S +R s 1928 o - Ap 15 0 1 S +R s 1929 o - Ap 20 23 1 S +R s 1937 o - Jun 16 23 1 S +R s 1937 o - O 2 24s 0 - +R s 1938 o - Ap 2 23 1 S +R s 1938 o - Ap 30 23 2 M +R s 1938 o - O 2 24 1 S +R s 1939 o - O 7 24s 0 - +R s 1942 o - May 2 23 1 S +R s 1942 o - S 1 1 0 - +R s 1943 1946 - Ap Sat>=13 23 1 S +R s 1943 1944 - O Sun>=1 1 0 - +R s 1945 1946 - S lastSun 1 0 - +R s 1949 o - Ap 30 23 1 S +R s 1949 o - O 2 1 0 - +R s 1974 1975 - Ap Sat>=12 23 1 S +R s 1974 1975 - O Sun>=1 1 0 - +R s 1976 o - Mar 27 23 1 S +R s 1976 1977 - S lastSun 1 0 - +R s 1977 o - Ap 2 23 1 S +R s 1978 o - Ap 2 2s 1 S +R s 1978 o - O 1 2s 0 - +R Sp 1967 o - Jun 3 12 1 S +R Sp 1967 o - O 1 0 0 - +R Sp 1974 o - Jun 24 0 1 S +R Sp 1974 o - S 1 0 0 - +R Sp 1976 1977 - May 1 0 1 S +R Sp 1976 o - Au 1 0 0 - +R Sp 1977 o - S 28 0 0 - +R Sp 1978 o - Jun 1 0 1 S +R Sp 1978 o - Au 4 0 0 - +Z Europe/Madrid -0:14:44 - LMT 1900 D 31 23:45:16 +0 s WE%sT 1940 Mar 16 23 +1 s CE%sT 1979 +1 E CE%sT +Z Africa/Ceuta -0:21:16 - LMT 1900 D 31 23:38:44 +0 - WET 1918 May 6 23 +0 1 WEST 1918 O 7 23 +0 - WET 1924 +0 s WE%sT 1929 +0 - WET 1967 +0 Sp WE%sT 1984 Mar 16 +1 - CET 1986 +1 E CE%sT +Z Atlantic/Canary -1:1:36 - LMT 1922 Mar +-1 - -01 1946 S 30 1 +0 - WET 1980 Ap 6 0s +0 1 WEST 1980 S 28 1u +0 E WE%sT +Z Europe/Stockholm 1:12:12 - LMT 1879 +1:0:14 - SET 1900 +1 - CET 1916 May 14 23 +1 1 CEST 1916 O 1 1 +1 - CET 1980 +1 E CE%sT +R CH 1941 1942 - May M>=1 1 1 S +R CH 1941 1942 - O M>=1 2 0 - +Z Europe/Zurich 0:34:8 - LMT 1853 Jul 16 +0:29:46 - BMT 1894 Jun +1 CH CE%sT 1981 +1 E CE%sT +R T 1916 o - May 1 0 1 S +R T 1916 o - O 1 0 0 - +R T 1920 o - Mar 28 0 1 S +R T 1920 o - O 25 0 0 - +R T 1921 o - Ap 3 0 1 S +R T 1921 o - O 3 0 0 - +R T 1922 o - Mar 26 0 1 S +R T 1922 o - O 8 0 0 - +R T 1924 o - May 13 0 1 S +R T 1924 1925 - O 1 0 0 - +R T 1925 o - May 1 0 1 S +R T 1940 o - Jun 30 0 1 S +R T 1940 o - O 5 0 0 - +R T 1940 o - D 1 0 1 S +R T 1941 o - S 21 0 0 - +R T 1942 o - Ap 1 0 1 S +R T 1942 o - N 1 0 0 - +R T 1945 o - Ap 2 0 1 S +R T 1945 o - O 8 0 0 - +R T 1946 o - Jun 1 0 1 S +R T 1946 o - O 1 0 0 - +R T 1947 1948 - Ap Sun>=16 0 1 S +R T 1947 1950 - O Sun>=2 0 0 - +R T 1949 o - Ap 10 0 1 S +R T 1950 o - Ap 19 0 1 S +R T 1951 o - Ap 22 0 1 S +R T 1951 o - O 8 0 0 - +R T 1962 o - Jul 15 0 1 S +R T 1962 o - O 8 0 0 - +R T 1964 o - May 15 0 1 S +R T 1964 o - O 1 0 0 - +R T 1970 1972 - May Sun>=2 0 1 S +R T 1970 1972 - O Sun>=2 0 0 - +R T 1973 o - Jun 3 1 1 S +R T 1973 o - N 4 3 0 - +R T 1974 o - Mar 31 2 1 S +R T 1974 o - N 3 5 0 - +R T 1975 o - Mar 30 0 1 S +R T 1975 1976 - O lastSun 0 0 - +R T 1976 o - Jun 1 0 1 S +R T 1977 1978 - Ap Sun>=1 0 1 S +R T 1977 o - O 16 0 0 - +R T 1979 1980 - Ap Sun>=1 3 1 S +R T 1979 1982 - O M>=11 0 0 - +R T 1981 1982 - Mar lastSun 3 1 S +R T 1983 o - Jul 31 0 1 S +R T 1983 o - O 2 0 0 - +R T 1985 o - Ap 20 0 1 S +R T 1985 o - S 28 0 0 - +R T 1986 1993 - Mar lastSun 1s 1 S +R T 1986 1995 - S lastSun 1s 0 - +R T 1994 o - Mar 20 1s 1 S +R T 1995 2006 - Mar lastSun 1s 1 S +R T 1996 2006 - O lastSun 1s 0 - +Z Europe/Istanbul 1:55:52 - LMT 1880 +1:56:56 - IMT 1910 O +2 T EE%sT 1978 O 15 +3 T +03/+04 1985 Ap 20 +2 T EE%sT 2007 +2 E EE%sT 2011 Mar 27 1u +2 - EET 2011 Mar 28 1u +2 E EE%sT 2014 Mar 30 1u +2 - EET 2014 Mar 31 1u +2 E EE%sT 2015 O 25 1u +2 1 EEST 2015 N 8 1u +2 E EE%sT 2016 S 7 +3 - +03 +Li Europe/Istanbul Asia/Istanbul +Z Europe/Kiev 2:2:4 - LMT 1880 +2:2:4 - KMT 1924 May 2 +2 - EET 1930 Jun 21 +3 - MSK 1941 S 20 +1 c CE%sT 1943 N 6 +3 R MSK/MSD 1990 Jul 1 2 +2 1 EEST 1991 S 29 3 +2 e EE%sT 1995 +2 E EE%sT +Z Europe/Uzhgorod 1:29:12 - LMT 1890 O +1 - CET 1940 +1 c CE%sT 1944 O +1 1 CEST 1944 O 26 +1 - CET 1945 Jun 29 +3 R MSK/MSD 1990 +3 - MSK 1990 Jul 1 2 +1 - CET 1991 Mar 31 3 +2 - EET 1992 +2 e EE%sT 1995 +2 E EE%sT +Z Europe/Zaporozhye 2:20:40 - LMT 1880 +2:20 - +0220 1924 May 2 +2 - EET 1930 Jun 21 +3 - MSK 1941 Au 25 +1 c CE%sT 1943 O 25 +3 R MSK/MSD 1991 Mar 31 2 +2 e EE%sT 1995 +2 E EE%sT +R u 1918 1919 - Mar lastSun 2 1 D +R u 1918 1919 - O lastSun 2 0 S +R u 1942 o - F 9 2 1 W +R u 1945 o - Au 14 23u 1 P +R u 1945 o - S lastSun 2 0 S +R u 1967 2006 - O lastSun 2 0 S +R u 1967 1973 - Ap lastSun 2 1 D +R u 1974 o - Ja 6 2 1 D +R u 1975 o - F 23 2 1 D +R u 1976 1986 - Ap lastSun 2 1 D +R u 1987 2006 - Ap Sun>=1 2 1 D +R u 2007 ma - Mar Sun>=8 2 1 D +R u 2007 ma - N Sun>=1 2 0 S +Z EST -5 - EST +Z MST -7 - MST +Z HST -10 - HST +Z EST5EDT -5 u E%sT +Z CST6CDT -6 u C%sT +Z MST7MDT -7 u M%sT +Z PST8PDT -8 u P%sT +R NY 1920 o - Mar lastSun 2 1 D +R NY 1920 o - O lastSun 2 0 S +R NY 1921 1966 - Ap lastSun 2 1 D +R NY 1921 1954 - S lastSun 2 0 S +R NY 1955 1966 - O lastSun 2 0 S +Z America/New_York -4:56:2 - LMT 1883 N 18 12:3:58 +-5 u E%sT 1920 +-5 NY E%sT 1942 +-5 u E%sT 1946 +-5 NY E%sT 1967 +-5 u E%sT +R Ch 1920 o - Jun 13 2 1 D +R Ch 1920 1921 - O lastSun 2 0 S +R Ch 1921 o - Mar lastSun 2 1 D +R Ch 1922 1966 - Ap lastSun 2 1 D +R Ch 1922 1954 - S lastSun 2 0 S +R Ch 1955 1966 - O lastSun 2 0 S +Z America/Chicago -5:50:36 - LMT 1883 N 18 12:9:24 +-6 u C%sT 1920 +-6 Ch C%sT 1936 Mar 1 2 +-5 - EST 1936 N 15 2 +-6 Ch C%sT 1942 +-6 u C%sT 1946 +-6 Ch C%sT 1967 +-6 u C%sT +Z America/North_Dakota/Center -6:45:12 - LMT 1883 N 18 12:14:48 +-7 u M%sT 1992 O 25 2 +-6 u C%sT +Z America/North_Dakota/New_Salem -6:45:39 - LMT 1883 N 18 12:14:21 +-7 u M%sT 2003 O 26 2 +-6 u C%sT +Z America/North_Dakota/Beulah -6:47:7 - LMT 1883 N 18 12:12:53 +-7 u M%sT 2010 N 7 2 +-6 u C%sT +R De 1920 1921 - Mar lastSun 2 1 D +R De 1920 o - O lastSun 2 0 S +R De 1921 o - May 22 2 0 S +R De 1965 1966 - Ap lastSun 2 1 D +R De 1965 1966 - O lastSun 2 0 S +Z America/Denver -6:59:56 - LMT 1883 N 18 12:0:4 +-7 u M%sT 1920 +-7 De M%sT 1942 +-7 u M%sT 1946 +-7 De M%sT 1967 +-7 u M%sT +R CA 1948 o - Mar 14 2:1 1 D +R CA 1949 o - Ja 1 2 0 S +R CA 1950 1966 - Ap lastSun 1 1 D +R CA 1950 1961 - S lastSun 2 0 S +R CA 1962 1966 - O lastSun 2 0 S +Z America/Los_Angeles -7:52:58 - LMT 1883 N 18 12:7:2 +-8 u P%sT 1946 +-8 CA P%sT 1967 +-8 u P%sT +Z America/Juneau 15:2:19 - LMT 1867 O 19 15:33:32 +-8:57:41 - LMT 1900 Au 20 12 +-8 - PST 1942 +-8 u P%sT 1946 +-8 - PST 1969 +-8 u P%sT 1980 Ap 27 2 +-9 u Y%sT 1980 O 26 2 +-8 u P%sT 1983 O 30 2 +-9 u Y%sT 1983 N 30 +-9 u AK%sT +Z America/Sitka 14:58:47 - LMT 1867 O 19 15:30 +-9:1:13 - LMT 1900 Au 20 12 +-8 - PST 1942 +-8 u P%sT 1946 +-8 - PST 1969 +-8 u P%sT 1983 O 30 2 +-9 u Y%sT 1983 N 30 +-9 u AK%sT +Z America/Metlakatla 15:13:42 - LMT 1867 O 19 15:44:55 +-8:46:18 - LMT 1900 Au 20 12 +-8 - PST 1942 +-8 u P%sT 1946 +-8 - PST 1969 +-8 u P%sT 1983 O 30 2 +-8 - PST 2015 N 1 2 +-9 u AK%sT +Z America/Yakutat 14:41:5 - LMT 1867 O 19 15:12:18 +-9:18:55 - LMT 1900 Au 20 12 +-9 - YST 1942 +-9 u Y%sT 1946 +-9 - YST 1969 +-9 u Y%sT 1983 N 30 +-9 u AK%sT +Z America/Anchorage 14:0:24 - LMT 1867 O 19 14:31:37 +-9:59:36 - LMT 1900 Au 20 12 +-10 - AST 1942 +-10 u A%sT 1967 Ap +-10 - AHST 1969 +-10 u AH%sT 1983 O 30 2 +-9 u Y%sT 1983 N 30 +-9 u AK%sT +Z America/Nome 12:58:22 - LMT 1867 O 19 13:29:35 +-11:1:38 - LMT 1900 Au 20 12 +-11 - NST 1942 +-11 u N%sT 1946 +-11 - NST 1967 Ap +-11 - BST 1969 +-11 u B%sT 1983 O 30 2 +-9 u Y%sT 1983 N 30 +-9 u AK%sT +Z America/Adak 12:13:22 - LMT 1867 O 19 12:44:35 +-11:46:38 - LMT 1900 Au 20 12 +-11 - NST 1942 +-11 u N%sT 1946 +-11 - NST 1967 Ap +-11 - BST 1969 +-11 u B%sT 1983 O 30 2 +-10 u AH%sT 1983 N 30 +-10 u H%sT +Z Pacific/Honolulu -10:31:26 - LMT 1896 Ja 13 12 +-10:30 - HST 1933 Ap 30 2 +-10:30 1 HDT 1933 May 21 12 +-10:30 u H%sT 1947 Jun 8 2 +-10 - HST +Z America/Phoenix -7:28:18 - LMT 1883 N 18 11:31:42 +-7 u M%sT 1944 Ja 1 0:1 +-7 - MST 1944 Ap 1 0:1 +-7 u M%sT 1944 O 1 0:1 +-7 - MST 1967 +-7 u M%sT 1968 Mar 21 +-7 - MST +Z America/Boise -7:44:49 - LMT 1883 N 18 12:15:11 +-8 u P%sT 1923 May 13 2 +-7 u M%sT 1974 +-7 - MST 1974 F 3 2 +-7 u M%sT +R In 1941 o - Jun 22 2 1 D +R In 1941 1954 - S lastSun 2 0 S +R In 1946 1954 - Ap lastSun 2 1 D +Z America/Indiana/Indianapolis -5:44:38 - LMT 1883 N 18 12:15:22 +-6 u C%sT 1920 +-6 In C%sT 1942 +-6 u C%sT 1946 +-6 In C%sT 1955 Ap 24 2 +-5 - EST 1957 S 29 2 +-6 - CST 1958 Ap 27 2 +-5 - EST 1969 +-5 u E%sT 1971 +-5 - EST 2006 +-5 u E%sT +R Ma 1951 o - Ap lastSun 2 1 D +R Ma 1951 o - S lastSun 2 0 S +R Ma 1954 1960 - Ap lastSun 2 1 D +R Ma 1954 1960 - S lastSun 2 0 S +Z America/Indiana/Marengo -5:45:23 - LMT 1883 N 18 12:14:37 +-6 u C%sT 1951 +-6 Ma C%sT 1961 Ap 30 2 +-5 - EST 1969 +-5 u E%sT 1974 Ja 6 2 +-6 1 CDT 1974 O 27 2 +-5 u E%sT 1976 +-5 - EST 2006 +-5 u E%sT +R V 1946 o - Ap lastSun 2 1 D +R V 1946 o - S lastSun 2 0 S +R V 1953 1954 - Ap lastSun 2 1 D +R V 1953 1959 - S lastSun 2 0 S +R V 1955 o - May 1 0 1 D +R V 1956 1963 - Ap lastSun 2 1 D +R V 1960 o - O lastSun 2 0 S +R V 1961 o - S lastSun 2 0 S +R V 1962 1963 - O lastSun 2 0 S +Z America/Indiana/Vincennes -5:50:7 - LMT 1883 N 18 12:9:53 +-6 u C%sT 1946 +-6 V C%sT 1964 Ap 26 2 +-5 - EST 1969 +-5 u E%sT 1971 +-5 - EST 2006 Ap 2 2 +-6 u C%sT 2007 N 4 2 +-5 u E%sT +R Pe 1946 o - Ap lastSun 2 1 D +R Pe 1946 o - S lastSun 2 0 S +R Pe 1953 1954 - Ap lastSun 2 1 D +R Pe 1953 1959 - S lastSun 2 0 S +R Pe 1955 o - May 1 0 1 D +R Pe 1956 1963 - Ap lastSun 2 1 D +R Pe 1960 o - O lastSun 2 0 S +R Pe 1961 o - S lastSun 2 0 S +R Pe 1962 1963 - O lastSun 2 0 S +Z America/Indiana/Tell_City -5:47:3 - LMT 1883 N 18 12:12:57 +-6 u C%sT 1946 +-6 Pe C%sT 1964 Ap 26 2 +-5 - EST 1969 +-5 u E%sT 1971 +-5 - EST 2006 Ap 2 2 +-6 u C%sT +R Pi 1955 o - May 1 0 1 D +R Pi 1955 1960 - S lastSun 2 0 S +R Pi 1956 1964 - Ap lastSun 2 1 D +R Pi 1961 1964 - O lastSun 2 0 S +Z America/Indiana/Petersburg -5:49:7 - LMT 1883 N 18 12:10:53 +-6 u C%sT 1955 +-6 Pi C%sT 1965 Ap 25 2 +-5 - EST 1966 O 30 2 +-6 u C%sT 1977 O 30 2 +-5 - EST 2006 Ap 2 2 +-6 u C%sT 2007 N 4 2 +-5 u E%sT +R St 1947 1961 - Ap lastSun 2 1 D +R St 1947 1954 - S lastSun 2 0 S +R St 1955 1956 - O lastSun 2 0 S +R St 1957 1958 - S lastSun 2 0 S +R St 1959 1961 - O lastSun 2 0 S +Z America/Indiana/Knox -5:46:30 - LMT 1883 N 18 12:13:30 +-6 u C%sT 1947 +-6 St C%sT 1962 Ap 29 2 +-5 - EST 1963 O 27 2 +-6 u C%sT 1991 O 27 2 +-5 - EST 2006 Ap 2 2 +-6 u C%sT +R Pu 1946 1960 - Ap lastSun 2 1 D +R Pu 1946 1954 - S lastSun 2 0 S +R Pu 1955 1956 - O lastSun 2 0 S +R Pu 1957 1960 - S lastSun 2 0 S +Z America/Indiana/Winamac -5:46:25 - LMT 1883 N 18 12:13:35 +-6 u C%sT 1946 +-6 Pu C%sT 1961 Ap 30 2 +-5 - EST 1969 +-5 u E%sT 1971 +-5 - EST 2006 Ap 2 2 +-6 u C%sT 2007 Mar 11 2 +-5 u E%sT +Z America/Indiana/Vevay -5:40:16 - LMT 1883 N 18 12:19:44 +-6 u C%sT 1954 Ap 25 2 +-5 - EST 1969 +-5 u E%sT 1973 +-5 - EST 2006 +-5 u E%sT +R v 1921 o - May 1 2 1 D +R v 1921 o - S 1 2 0 S +R v 1941 1961 - Ap lastSun 2 1 D +R v 1941 o - S lastSun 2 0 S +R v 1946 o - Jun 2 2 0 S +R v 1950 1955 - S lastSun 2 0 S +R v 1956 1960 - O lastSun 2 0 S +Z America/Kentucky/Louisville -5:43:2 - LMT 1883 N 18 12:16:58 +-6 u C%sT 1921 +-6 v C%sT 1942 +-6 u C%sT 1946 +-6 v C%sT 1961 Jul 23 2 +-5 - EST 1968 +-5 u E%sT 1974 Ja 6 2 +-6 1 CDT 1974 O 27 2 +-5 u E%sT +Z America/Kentucky/Monticello -5:39:24 - LMT 1883 N 18 12:20:36 +-6 u C%sT 1946 +-6 - CST 1968 +-6 u C%sT 2000 O 29 2 +-5 u E%sT +R Dt 1948 o - Ap lastSun 2 1 D +R Dt 1948 o - S lastSun 2 0 S +Z America/Detroit -5:32:11 - LMT 1905 +-6 - CST 1915 May 15 2 +-5 - EST 1942 +-5 u E%sT 1946 +-5 Dt E%sT 1973 +-5 u E%sT 1975 +-5 - EST 1975 Ap 27 2 +-5 u E%sT +R Me 1946 o - Ap lastSun 2 1 D +R Me 1946 o - S lastSun 2 0 S +R Me 1966 o - Ap lastSun 2 1 D +R Me 1966 o - O lastSun 2 0 S +Z America/Menominee -5:50:27 - LMT 1885 S 18 12 +-6 u C%sT 1946 +-6 Me C%sT 1969 Ap 27 2 +-5 - EST 1973 Ap 29 2 +-6 u C%sT +R C 1918 o - Ap 14 2 1 D +R C 1918 o - O 27 2 0 S +R C 1942 o - F 9 2 1 W +R C 1945 o - Au 14 23u 1 P +R C 1945 o - S 30 2 0 S +R C 1974 1986 - Ap lastSun 2 1 D +R C 1974 2006 - O lastSun 2 0 S +R C 1987 2006 - Ap Sun>=1 2 1 D +R C 2007 ma - Mar Sun>=8 2 1 D +R C 2007 ma - N Sun>=1 2 0 S +R j 1917 o - Ap 8 2 1 D +R j 1917 o - S 17 2 0 S +R j 1919 o - May 5 23 1 D +R j 1919 o - Au 12 23 0 S +R j 1920 1935 - May Sun>=1 23 1 D +R j 1920 1935 - O lastSun 23 0 S +R j 1936 1941 - May M>=9 0 1 D +R j 1936 1941 - O M>=2 0 0 S +R j 1946 1950 - May Sun>=8 2 1 D +R j 1946 1950 - O Sun>=2 2 0 S +R j 1951 1986 - Ap lastSun 2 1 D +R j 1951 1959 - S lastSun 2 0 S +R j 1960 1986 - O lastSun 2 0 S +R j 1987 o - Ap Sun>=1 0:1 1 D +R j 1987 2006 - O lastSun 0:1 0 S +R j 1988 o - Ap Sun>=1 0:1 2 DD +R j 1989 2006 - Ap Sun>=1 0:1 1 D +R j 2007 2011 - Mar Sun>=8 0:1 1 D +R j 2007 2010 - N Sun>=1 0:1 0 S +Z America/St_Johns -3:30:52 - LMT 1884 +-3:30:52 j N%sT 1918 +-3:30:52 C N%sT 1919 +-3:30:52 j N%sT 1935 Mar 30 +-3:30 j N%sT 1942 May 11 +-3:30 C N%sT 1946 +-3:30 j N%sT 2011 N +-3:30 C N%sT +Z America/Goose_Bay -4:1:40 - LMT 1884 +-3:30:52 - NST 1918 +-3:30:52 C N%sT 1919 +-3:30:52 - NST 1935 Mar 30 +-3:30 - NST 1936 +-3:30 j N%sT 1942 May 11 +-3:30 C N%sT 1946 +-3:30 j N%sT 1966 Mar 15 2 +-4 j A%sT 2011 N +-4 C A%sT +R H 1916 o - Ap 1 0 1 D +R H 1916 o - O 1 0 0 S +R H 1920 o - May 9 0 1 D +R H 1920 o - Au 29 0 0 S +R H 1921 o - May 6 0 1 D +R H 1921 1922 - S 5 0 0 S +R H 1922 o - Ap 30 0 1 D +R H 1923 1925 - May Sun>=1 0 1 D +R H 1923 o - S 4 0 0 S +R H 1924 o - S 15 0 0 S +R H 1925 o - S 28 0 0 S +R H 1926 o - May 16 0 1 D +R H 1926 o - S 13 0 0 S +R H 1927 o - May 1 0 1 D +R H 1927 o - S 26 0 0 S +R H 1928 1931 - May Sun>=8 0 1 D +R H 1928 o - S 9 0 0 S +R H 1929 o - S 3 0 0 S +R H 1930 o - S 15 0 0 S +R H 1931 1932 - S M>=24 0 0 S +R H 1932 o - May 1 0 1 D +R H 1933 o - Ap 30 0 1 D +R H 1933 o - O 2 0 0 S +R H 1934 o - May 20 0 1 D +R H 1934 o - S 16 0 0 S +R H 1935 o - Jun 2 0 1 D +R H 1935 o - S 30 0 0 S +R H 1936 o - Jun 1 0 1 D +R H 1936 o - S 14 0 0 S +R H 1937 1938 - May Sun>=1 0 1 D +R H 1937 1941 - S M>=24 0 0 S +R H 1939 o - May 28 0 1 D +R H 1940 1941 - May Sun>=1 0 1 D +R H 1946 1949 - Ap lastSun 2 1 D +R H 1946 1949 - S lastSun 2 0 S +R H 1951 1954 - Ap lastSun 2 1 D +R H 1951 1954 - S lastSun 2 0 S +R H 1956 1959 - Ap lastSun 2 1 D +R H 1956 1959 - S lastSun 2 0 S +R H 1962 1973 - Ap lastSun 2 1 D +R H 1962 1973 - O lastSun 2 0 S +Z America/Halifax -4:14:24 - LMT 1902 Jun 15 +-4 H A%sT 1918 +-4 C A%sT 1919 +-4 H A%sT 1942 F 9 2s +-4 C A%sT 1946 +-4 H A%sT 1974 +-4 C A%sT +Z America/Glace_Bay -3:59:48 - LMT 1902 Jun 15 +-4 C A%sT 1953 +-4 H A%sT 1954 +-4 - AST 1972 +-4 H A%sT 1974 +-4 C A%sT +R o 1933 1935 - Jun Sun>=8 1 1 D +R o 1933 1935 - S Sun>=8 1 0 S +R o 1936 1938 - Jun Sun>=1 1 1 D +R o 1936 1938 - S Sun>=1 1 0 S +R o 1939 o - May 27 1 1 D +R o 1939 1941 - S Sat>=21 1 0 S +R o 1940 o - May 19 1 1 D +R o 1941 o - May 4 1 1 D +R o 1946 1972 - Ap lastSun 2 1 D +R o 1946 1956 - S lastSun 2 0 S +R o 1957 1972 - O lastSun 2 0 S +R o 1993 2006 - Ap Sun>=1 0:1 1 D +R o 1993 2006 - O lastSun 0:1 0 S +Z America/Moncton -4:19:8 - LMT 1883 D 9 +-5 - EST 1902 Jun 15 +-4 C A%sT 1933 +-4 o A%sT 1942 +-4 C A%sT 1946 +-4 o A%sT 1973 +-4 C A%sT 1993 +-4 o A%sT 2007 +-4 C A%sT +Z America/Blanc-Sablon -3:48:28 - LMT 1884 +-4 C A%sT 1970 +-4 - AST +R t 1919 o - Mar 30 23:30 1 D +R t 1919 o - O 26 0 0 S +R t 1920 o - May 2 2 1 D +R t 1920 o - S 26 0 0 S +R t 1921 o - May 15 2 1 D +R t 1921 o - S 15 2 0 S +R t 1922 1923 - May Sun>=8 2 1 D +R t 1922 1926 - S Sun>=15 2 0 S +R t 1924 1927 - May Sun>=1 2 1 D +R t 1927 1932 - S lastSun 2 0 S +R t 1928 1931 - Ap lastSun 2 1 D +R t 1932 o - May 1 2 1 D +R t 1933 1940 - Ap lastSun 2 1 D +R t 1933 o - O 1 2 0 S +R t 1934 1939 - S lastSun 2 0 S +R t 1945 1946 - S lastSun 2 0 S +R t 1946 o - Ap lastSun 2 1 D +R t 1947 1949 - Ap lastSun 0 1 D +R t 1947 1948 - S lastSun 0 0 S +R t 1949 o - N lastSun 0 0 S +R t 1950 1973 - Ap lastSun 2 1 D +R t 1950 o - N lastSun 2 0 S +R t 1951 1956 - S lastSun 2 0 S +R t 1957 1973 - O lastSun 2 0 S +Z America/Toronto -5:17:32 - LMT 1895 +-5 C E%sT 1919 +-5 t E%sT 1942 F 9 2s +-5 C E%sT 1946 +-5 t E%sT 1974 +-5 C E%sT +Z America/Thunder_Bay -5:57 - LMT 1895 +-6 - CST 1910 +-5 - EST 1942 +-5 C E%sT 1970 +-5 t E%sT 1973 +-5 - EST 1974 +-5 C E%sT +Z America/Nipigon -5:53:4 - LMT 1895 +-5 C E%sT 1940 S 29 +-5 1 EDT 1942 F 9 2s +-5 C E%sT +Z America/Rainy_River -6:18:16 - LMT 1895 +-6 C C%sT 1940 S 29 +-6 1 CDT 1942 F 9 2s +-6 C C%sT +Z America/Atikokan -6:6:28 - LMT 1895 +-6 C C%sT 1940 S 29 +-6 1 CDT 1942 F 9 2s +-6 C C%sT 1945 S 30 2 +-5 - EST +R W 1916 o - Ap 23 0 1 D +R W 1916 o - S 17 0 0 S +R W 1918 o - Ap 14 2 1 D +R W 1918 o - O 27 2 0 S +R W 1937 o - May 16 2 1 D +R W 1937 o - S 26 2 0 S +R W 1942 o - F 9 2 1 W +R W 1945 o - Au 14 23u 1 P +R W 1945 o - S lastSun 2 0 S +R W 1946 o - May 12 2 1 D +R W 1946 o - O 13 2 0 S +R W 1947 1949 - Ap lastSun 2 1 D +R W 1947 1949 - S lastSun 2 0 S +R W 1950 o - May 1 2 1 D +R W 1950 o - S 30 2 0 S +R W 1951 1960 - Ap lastSun 2 1 D +R W 1951 1958 - S lastSun 2 0 S +R W 1959 o - O lastSun 2 0 S +R W 1960 o - S lastSun 2 0 S +R W 1963 o - Ap lastSun 2 1 D +R W 1963 o - S 22 2 0 S +R W 1966 1986 - Ap lastSun 2s 1 D +R W 1966 2005 - O lastSun 2s 0 S +R W 1987 2005 - Ap Sun>=1 2s 1 D +Z America/Winnipeg -6:28:36 - LMT 1887 Jul 16 +-6 W C%sT 2006 +-6 C C%sT +R r 1918 o - Ap 14 2 1 D +R r 1918 o - O 27 2 0 S +R r 1930 1934 - May Sun>=1 0 1 D +R r 1930 1934 - O Sun>=1 0 0 S +R r 1937 1941 - Ap Sun>=8 0 1 D +R r 1937 o - O Sun>=8 0 0 S +R r 1938 o - O Sun>=1 0 0 S +R r 1939 1941 - O Sun>=8 0 0 S +R r 1942 o - F 9 2 1 W +R r 1945 o - Au 14 23u 1 P +R r 1945 o - S lastSun 2 0 S +R r 1946 o - Ap Sun>=8 2 1 D +R r 1946 o - O Sun>=8 2 0 S +R r 1947 1957 - Ap lastSun 2 1 D +R r 1947 1957 - S lastSun 2 0 S +R r 1959 o - Ap lastSun 2 1 D +R r 1959 o - O lastSun 2 0 S +R Sw 1957 o - Ap lastSun 2 1 D +R Sw 1957 o - O lastSun 2 0 S +R Sw 1959 1961 - Ap lastSun 2 1 D +R Sw 1959 o - O lastSun 2 0 S +R Sw 1960 1961 - S lastSun 2 0 S +Z America/Regina -6:58:36 - LMT 1905 S +-7 r M%sT 1960 Ap lastSun 2 +-6 - CST +Z America/Swift_Current -7:11:20 - LMT 1905 S +-7 C M%sT 1946 Ap lastSun 2 +-7 r M%sT 1950 +-7 Sw M%sT 1972 Ap lastSun 2 +-6 - CST +R Ed 1918 1919 - Ap Sun>=8 2 1 D +R Ed 1918 o - O 27 2 0 S +R Ed 1919 o - May 27 2 0 S +R Ed 1920 1923 - Ap lastSun 2 1 D +R Ed 1920 o - O lastSun 2 0 S +R Ed 1921 1923 - S lastSun 2 0 S +R Ed 1942 o - F 9 2 1 W +R Ed 1945 o - Au 14 23u 1 P +R Ed 1945 o - S lastSun 2 0 S +R Ed 1947 o - Ap lastSun 2 1 D +R Ed 1947 o - S lastSun 2 0 S +R Ed 1967 o - Ap lastSun 2 1 D +R Ed 1967 o - O lastSun 2 0 S +R Ed 1969 o - Ap lastSun 2 1 D +R Ed 1969 o - O lastSun 2 0 S +R Ed 1972 1986 - Ap lastSun 2 1 D +R Ed 1972 2006 - O lastSun 2 0 S +Z America/Edmonton -7:33:52 - LMT 1906 S +-7 Ed M%sT 1987 +-7 C M%sT +R Va 1918 o - Ap 14 2 1 D +R Va 1918 o - O 27 2 0 S +R Va 1942 o - F 9 2 1 W +R Va 1945 o - Au 14 23u 1 P +R Va 1945 o - S 30 2 0 S +R Va 1946 1986 - Ap lastSun 2 1 D +R Va 1946 o - O 13 2 0 S +R Va 1947 1961 - S lastSun 2 0 S +R Va 1962 2006 - O lastSun 2 0 S +Z America/Vancouver -8:12:28 - LMT 1884 +-8 Va P%sT 1987 +-8 C P%sT +Z America/Dawson_Creek -8:0:56 - LMT 1884 +-8 C P%sT 1947 +-8 Va P%sT 1972 Au 30 2 +-7 - MST +Z America/Fort_Nelson -8:10:47 - LMT 1884 +-8 Va P%sT 1946 +-8 - PST 1947 +-8 Va P%sT 1987 +-8 C P%sT 2015 Mar 8 2 +-7 - MST +Z America/Creston -7:46:4 - LMT 1884 +-7 - MST 1916 O +-8 - PST 1918 Jun 2 +-7 - MST +R Y 1918 o - Ap 14 2 1 D +R Y 1918 o - O 27 2 0 S +R Y 1919 o - May 25 2 1 D +R Y 1919 o - N 1 0 0 S +R Y 1942 o - F 9 2 1 W +R Y 1945 o - Au 14 23u 1 P +R Y 1945 o - S 30 2 0 S +R Y 1965 o - Ap lastSun 0 2 DD +R Y 1965 o - O lastSun 2 0 S +R Y 1980 1986 - Ap lastSun 2 1 D +R Y 1980 2006 - O lastSun 2 0 S +R Y 1987 2006 - Ap Sun>=1 2 1 D +Z America/Pangnirtung 0 - -00 1921 +-4 Y A%sT 1995 Ap Sun>=1 2 +-5 C E%sT 1999 O 31 2 +-6 C C%sT 2000 O 29 2 +-5 C E%sT +Z America/Iqaluit 0 - -00 1942 Au +-5 Y E%sT 1999 O 31 2 +-6 C C%sT 2000 O 29 2 +-5 C E%sT +Z America/Resolute 0 - -00 1947 Au 31 +-6 Y C%sT 2000 O 29 2 +-5 - EST 2001 Ap 1 3 +-6 C C%sT 2006 O 29 2 +-5 - EST 2007 Mar 11 3 +-6 C C%sT +Z America/Rankin_Inlet 0 - -00 1957 +-6 Y C%sT 2000 O 29 2 +-5 - EST 2001 Ap 1 3 +-6 C C%sT +Z America/Cambridge_Bay 0 - -00 1920 +-7 Y M%sT 1999 O 31 2 +-6 C C%sT 2000 O 29 2 +-5 - EST 2000 N 5 +-6 - CST 2001 Ap 1 3 +-7 C M%sT +Z America/Yellowknife 0 - -00 1935 +-7 Y M%sT 1980 +-7 C M%sT +Z America/Inuvik 0 - -00 1953 +-8 Y P%sT 1979 Ap lastSun 2 +-7 Y M%sT 1980 +-7 C M%sT +Z America/Whitehorse -9:0:12 - LMT 1900 Au 20 +-9 Y Y%sT 1967 May 28 +-8 Y P%sT 1980 +-8 C P%sT +Z America/Dawson -9:17:40 - LMT 1900 Au 20 +-9 Y Y%sT 1973 O 28 +-8 Y P%sT 1980 +-8 C P%sT +R m 1939 o - F 5 0 1 D +R m 1939 o - Jun 25 0 0 S +R m 1940 o - D 9 0 1 D +R m 1941 o - Ap 1 0 0 S +R m 1943 o - D 16 0 1 W +R m 1944 o - May 1 0 0 S +R m 1950 o - F 12 0 1 D +R m 1950 o - Jul 30 0 0 S +R m 1996 2000 - Ap Sun>=1 2 1 D +R m 1996 2000 - O lastSun 2 0 S +R m 2001 o - May Sun>=1 2 1 D +R m 2001 o - S lastSun 2 0 S +R m 2002 ma - Ap Sun>=1 2 1 D +R m 2002 ma - O lastSun 2 0 S +Z America/Cancun -5:47:4 - LMT 1922 Ja 1 0:12:56 +-6 - CST 1981 D 23 +-5 m E%sT 1998 Au 2 2 +-6 m C%sT 2015 F 1 2 +-5 - EST +Z America/Merida -5:58:28 - LMT 1922 Ja 1 0:1:32 +-6 - CST 1981 D 23 +-5 - EST 1982 D 2 +-6 m C%sT +Z America/Matamoros -6:40 - LMT 1921 D 31 23:20 +-6 - CST 1988 +-6 u C%sT 1989 +-6 m C%sT 2010 +-6 u C%sT +Z America/Monterrey -6:41:16 - LMT 1921 D 31 23:18:44 +-6 - CST 1988 +-6 u C%sT 1989 +-6 m C%sT +Z America/Mexico_City -6:36:36 - LMT 1922 Ja 1 0:23:24 +-7 - MST 1927 Jun 10 23 +-6 - CST 1930 N 15 +-7 - MST 1931 May 1 23 +-6 - CST 1931 O +-7 - MST 1932 Ap +-6 m C%sT 2001 S 30 2 +-6 - CST 2002 F 20 +-6 m C%sT +Z America/Ojinaga -6:57:40 - LMT 1922 Ja 1 0:2:20 +-7 - MST 1927 Jun 10 23 +-6 - CST 1930 N 15 +-7 - MST 1931 May 1 23 +-6 - CST 1931 O +-7 - MST 1932 Ap +-6 - CST 1996 +-6 m C%sT 1998 +-6 - CST 1998 Ap Sun>=1 3 +-7 m M%sT 2010 +-7 u M%sT +Z America/Chihuahua -7:4:20 - LMT 1921 D 31 23:55:40 +-7 - MST 1927 Jun 10 23 +-6 - CST 1930 N 15 +-7 - MST 1931 May 1 23 +-6 - CST 1931 O +-7 - MST 1932 Ap +-6 - CST 1996 +-6 m C%sT 1998 +-6 - CST 1998 Ap Sun>=1 3 +-7 m M%sT +Z America/Hermosillo -7:23:52 - LMT 1921 D 31 23:36:8 +-7 - MST 1927 Jun 10 23 +-6 - CST 1930 N 15 +-7 - MST 1931 May 1 23 +-6 - CST 1931 O +-7 - MST 1932 Ap +-6 - CST 1942 Ap 24 +-7 - MST 1949 Ja 14 +-8 - PST 1970 +-7 m M%sT 1999 +-7 - MST +Z America/Mazatlan -7:5:40 - LMT 1921 D 31 23:54:20 +-7 - MST 1927 Jun 10 23 +-6 - CST 1930 N 15 +-7 - MST 1931 May 1 23 +-6 - CST 1931 O +-7 - MST 1932 Ap +-6 - CST 1942 Ap 24 +-7 - MST 1949 Ja 14 +-8 - PST 1970 +-7 m M%sT +Z America/Bahia_Banderas -7:1 - LMT 1921 D 31 23:59 +-7 - MST 1927 Jun 10 23 +-6 - CST 1930 N 15 +-7 - MST 1931 May 1 23 +-6 - CST 1931 O +-7 - MST 1932 Ap +-6 - CST 1942 Ap 24 +-7 - MST 1949 Ja 14 +-8 - PST 1970 +-7 m M%sT 2010 Ap 4 2 +-6 m C%sT +Z America/Tijuana -7:48:4 - LMT 1922 Ja 1 0:11:56 +-7 - MST 1924 +-8 - PST 1927 Jun 10 23 +-7 - MST 1930 N 15 +-8 - PST 1931 Ap +-8 1 PDT 1931 S 30 +-8 - PST 1942 Ap 24 +-8 1 PWT 1945 Au 14 23u +-8 1 PPT 1945 N 12 +-8 - PST 1948 Ap 5 +-8 1 PDT 1949 Ja 14 +-8 - PST 1954 +-8 CA P%sT 1961 +-8 - PST 1976 +-8 u P%sT 1996 +-8 m P%sT 2001 +-8 u P%sT 2002 F 20 +-8 m P%sT 2010 +-8 u P%sT +R BS 1964 1975 - O lastSun 2 0 S +R BS 1964 1975 - Ap lastSun 2 1 D +Z America/Nassau -5:9:30 - LMT 1912 Mar 2 +-5 BS E%sT 1976 +-5 u E%sT +R BB 1977 o - Jun 12 2 1 D +R BB 1977 1978 - O Sun>=1 2 0 S +R BB 1978 1980 - Ap Sun>=15 2 1 D +R BB 1979 o - S 30 2 0 S +R BB 1980 o - S 25 2 0 S +Z America/Barbados -3:58:29 - LMT 1924 +-3:58:29 - BMT 1932 +-4 BB A%sT +R BZ 1918 1942 - O Sun>=2 0 0:30 -0530 +R BZ 1919 1943 - F Sun>=9 0 0 CST +R BZ 1973 o - D 5 0 1 CDT +R BZ 1974 o - F 9 0 0 CST +R BZ 1982 o - D 18 0 1 CDT +R BZ 1983 o - F 12 0 0 CST +Z America/Belize -5:52:48 - LMT 1912 Ap +-6 BZ %s +Z Atlantic/Bermuda -4:19:18 - LMT 1930 Ja 1 2 +-4 - AST 1974 Ap 28 2 +-4 C A%sT 1976 +-4 u A%sT +R CR 1979 1980 - F lastSun 0 1 D +R CR 1979 1980 - Jun Sun>=1 0 0 S +R CR 1991 1992 - Ja Sat>=15 0 1 D +R CR 1991 o - Jul 1 0 0 S +R CR 1992 o - Mar 15 0 0 S +Z America/Costa_Rica -5:36:13 - LMT 1890 +-5:36:13 - SJMT 1921 Ja 15 +-6 CR C%sT +R Q 1928 o - Jun 10 0 1 D +R Q 1928 o - O 10 0 0 S +R Q 1940 1942 - Jun Sun>=1 0 1 D +R Q 1940 1942 - S Sun>=1 0 0 S +R Q 1945 1946 - Jun Sun>=1 0 1 D +R Q 1945 1946 - S Sun>=1 0 0 S +R Q 1965 o - Jun 1 0 1 D +R Q 1965 o - S 30 0 0 S +R Q 1966 o - May 29 0 1 D +R Q 1966 o - O 2 0 0 S +R Q 1967 o - Ap 8 0 1 D +R Q 1967 1968 - S Sun>=8 0 0 S +R Q 1968 o - Ap 14 0 1 D +R Q 1969 1977 - Ap lastSun 0 1 D +R Q 1969 1971 - O lastSun 0 0 S +R Q 1972 1974 - O 8 0 0 S +R Q 1975 1977 - O lastSun 0 0 S +R Q 1978 o - May 7 0 1 D +R Q 1978 1990 - O Sun>=8 0 0 S +R Q 1979 1980 - Mar Sun>=15 0 1 D +R Q 1981 1985 - May Sun>=5 0 1 D +R Q 1986 1989 - Mar Sun>=14 0 1 D +R Q 1990 1997 - Ap Sun>=1 0 1 D +R Q 1991 1995 - O Sun>=8 0s 0 S +R Q 1996 o - O 6 0s 0 S +R Q 1997 o - O 12 0s 0 S +R Q 1998 1999 - Mar lastSun 0s 1 D +R Q 1998 2003 - O lastSun 0s 0 S +R Q 2000 2003 - Ap Sun>=1 0s 1 D +R Q 2004 o - Mar lastSun 0s 1 D +R Q 2006 2010 - O lastSun 0s 0 S +R Q 2007 o - Mar Sun>=8 0s 1 D +R Q 2008 o - Mar Sun>=15 0s 1 D +R Q 2009 2010 - Mar Sun>=8 0s 1 D +R Q 2011 o - Mar Sun>=15 0s 1 D +R Q 2011 o - N 13 0s 0 S +R Q 2012 o - Ap 1 0s 1 D +R Q 2012 ma - N Sun>=1 0s 0 S +R Q 2013 ma - Mar Sun>=8 0s 1 D +Z America/Havana -5:29:28 - LMT 1890 +-5:29:36 - HMT 1925 Jul 19 12 +-5 Q C%sT +R DO 1966 o - O 30 0 1 EDT +R DO 1967 o - F 28 0 0 EST +R DO 1969 1973 - O lastSun 0 0:30 -0430 +R DO 1970 o - F 21 0 0 EST +R DO 1971 o - Ja 20 0 0 EST +R DO 1972 1974 - Ja 21 0 0 EST +Z America/Santo_Domingo -4:39:36 - LMT 1890 +-4:40 - SDMT 1933 Ap 1 12 +-5 DO %s 1974 O 27 +-4 - AST 2000 O 29 2 +-5 u E%sT 2000 D 3 1 +-4 - AST +R SV 1987 1988 - May Sun>=1 0 1 D +R SV 1987 1988 - S lastSun 0 0 S +Z America/El_Salvador -5:56:48 - LMT 1921 +-6 SV C%sT +R GT 1973 o - N 25 0 1 D +R GT 1974 o - F 24 0 0 S +R GT 1983 o - May 21 0 1 D +R GT 1983 o - S 22 0 0 S +R GT 1991 o - Mar 23 0 1 D +R GT 1991 o - S 7 0 0 S +R GT 2006 o - Ap 30 0 1 D +R GT 2006 o - O 1 0 0 S +Z America/Guatemala -6:2:4 - LMT 1918 O 5 +-6 GT C%sT +R HT 1983 o - May 8 0 1 D +R HT 1984 1987 - Ap lastSun 0 1 D +R HT 1983 1987 - O lastSun 0 0 S +R HT 1988 1997 - Ap Sun>=1 1s 1 D +R HT 1988 1997 - O lastSun 1s 0 S +R HT 2005 2006 - Ap Sun>=1 0 1 D +R HT 2005 2006 - O lastSun 0 0 S +R HT 2012 2015 - Mar Sun>=8 2 1 D +R HT 2012 2015 - N Sun>=1 2 0 S +R HT 2017 ma - Mar Sun>=8 2 1 D +R HT 2017 ma - N Sun>=1 2 0 S +Z America/Port-au-Prince -4:49:20 - LMT 1890 +-4:49 - PPMT 1917 Ja 24 12 +-5 HT E%sT +R HN 1987 1988 - May Sun>=1 0 1 D +R HN 1987 1988 - S lastSun 0 0 S +R HN 2006 o - May Sun>=1 0 1 D +R HN 2006 o - Au M>=1 0 0 S +Z America/Tegucigalpa -5:48:52 - LMT 1921 Ap +-6 HN C%sT +Z America/Jamaica -5:7:10 - LMT 1890 +-5:7:10 - KMT 1912 F +-5 - EST 1974 +-5 u E%sT 1984 +-5 - EST +Z America/Martinique -4:4:20 - LMT 1890 +-4:4:20 - FFMT 1911 May +-4 - AST 1980 Ap 6 +-4 1 ADT 1980 S 28 +-4 - AST +R NI 1979 1980 - Mar Sun>=16 0 1 D +R NI 1979 1980 - Jun M>=23 0 0 S +R NI 2005 o - Ap 10 0 1 D +R NI 2005 o - O Sun>=1 0 0 S +R NI 2006 o - Ap 30 2 1 D +R NI 2006 o - O Sun>=1 1 0 S +Z America/Managua -5:45:8 - LMT 1890 +-5:45:12 - MMT 1934 Jun 23 +-6 - CST 1973 May +-5 - EST 1975 F 16 +-6 NI C%sT 1992 Ja 1 4 +-5 - EST 1992 S 24 +-6 - CST 1993 +-5 - EST 1997 +-6 NI C%sT +Z America/Panama -5:18:8 - LMT 1890 +-5:19:36 - CMT 1908 Ap 22 +-5 - EST +Li America/Panama America/Cayman +Z America/Puerto_Rico -4:24:25 - LMT 1899 Mar 28 12 +-4 - AST 1942 May 3 +-4 u A%sT 1946 +-4 - AST +Z America/Miquelon -3:44:40 - LMT 1911 May 15 +-4 - AST 1980 May +-3 - -03 1987 +-3 C -03/-02 +Z America/Grand_Turk -4:44:32 - LMT 1890 +-5:7:10 - KMT 1912 F +-5 - EST 1979 +-5 u E%sT 2015 N Sun>=1 2 +-4 - AST 2018 Mar 11 3 +-5 u E%sT +R A 1930 o - D 1 0 1 - +R A 1931 o - Ap 1 0 0 - +R A 1931 o - O 15 0 1 - +R A 1932 1940 - Mar 1 0 0 - +R A 1932 1939 - N 1 0 1 - +R A 1940 o - Jul 1 0 1 - +R A 1941 o - Jun 15 0 0 - +R A 1941 o - O 15 0 1 - +R A 1943 o - Au 1 0 0 - +R A 1943 o - O 15 0 1 - +R A 1946 o - Mar 1 0 0 - +R A 1946 o - O 1 0 1 - +R A 1963 o - O 1 0 0 - +R A 1963 o - D 15 0 1 - +R A 1964 1966 - Mar 1 0 0 - +R A 1964 1966 - O 15 0 1 - +R A 1967 o - Ap 2 0 0 - +R A 1967 1968 - O Sun>=1 0 1 - +R A 1968 1969 - Ap Sun>=1 0 0 - +R A 1974 o - Ja 23 0 1 - +R A 1974 o - May 1 0 0 - +R A 1988 o - D 1 0 1 - +R A 1989 1993 - Mar Sun>=1 0 0 - +R A 1989 1992 - O Sun>=15 0 1 - +R A 1999 o - O Sun>=1 0 1 - +R A 2000 o - Mar 3 0 0 - +R A 2007 o - D 30 0 1 - +R A 2008 2009 - Mar Sun>=15 0 0 - +R A 2008 o - O Sun>=15 0 1 - +Z America/Argentina/Buenos_Aires -3:53:48 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 A -03/-02 +Z America/Argentina/Cordoba -4:16:48 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1991 Mar 3 +-4 - -04 1991 O 20 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 A -03/-02 +Z America/Argentina/Salta -4:21:40 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1991 Mar 3 +-4 - -04 1991 O 20 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 A -03/-02 2008 O 18 +-3 - -03 +Z America/Argentina/Tucuman -4:20:52 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1991 Mar 3 +-4 - -04 1991 O 20 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 - -03 2004 Jun +-4 - -04 2004 Jun 13 +-3 A -03/-02 +Z America/Argentina/La_Rioja -4:27:24 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1991 Mar +-4 - -04 1991 May 7 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 - -03 2004 Jun +-4 - -04 2004 Jun 20 +-3 A -03/-02 2008 O 18 +-3 - -03 +Z America/Argentina/San_Juan -4:34:4 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1991 Mar +-4 - -04 1991 May 7 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 - -03 2004 May 31 +-4 - -04 2004 Jul 25 +-3 A -03/-02 2008 O 18 +-3 - -03 +Z America/Argentina/Jujuy -4:21:12 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1990 Mar 4 +-4 - -04 1990 O 28 +-4 1 -03 1991 Mar 17 +-4 - -04 1991 O 6 +-3 1 -02 1992 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 A -03/-02 2008 O 18 +-3 - -03 +Z America/Argentina/Catamarca -4:23:8 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1991 Mar 3 +-4 - -04 1991 O 20 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 - -03 2004 Jun +-4 - -04 2004 Jun 20 +-3 A -03/-02 2008 O 18 +-3 - -03 +Z America/Argentina/Mendoza -4:35:16 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1990 Mar 4 +-4 - -04 1990 O 15 +-4 1 -03 1991 Mar +-4 - -04 1991 O 15 +-4 1 -03 1992 Mar +-4 - -04 1992 O 18 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 - -03 2004 May 23 +-4 - -04 2004 S 26 +-3 A -03/-02 2008 O 18 +-3 - -03 +R Sa 2008 2009 - Mar Sun>=8 0 0 - +R Sa 2007 2008 - O Sun>=8 0 1 - +Z America/Argentina/San_Luis -4:25:24 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1990 +-3 1 -02 1990 Mar 14 +-4 - -04 1990 O 15 +-4 1 -03 1991 Mar +-4 - -04 1991 Jun +-3 - -03 1999 O 3 +-4 1 -03 2000 Mar 3 +-3 - -03 2004 May 31 +-4 - -04 2004 Jul 25 +-3 A -03/-02 2008 Ja 21 +-4 Sa -04/-03 2009 O 11 +-3 - -03 +Z America/Argentina/Rio_Gallegos -4:36:52 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 - -03 2004 Jun +-4 - -04 2004 Jun 20 +-3 A -03/-02 2008 O 18 +-3 - -03 +Z America/Argentina/Ushuaia -4:33:12 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - -04 1930 D +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1999 O 3 +-4 A -04/-03 2000 Mar 3 +-3 - -03 2004 May 30 +-4 - -04 2004 Jun 20 +-3 A -03/-02 2008 O 18 +-3 - -03 +Li America/Curacao America/Aruba +Z America/La_Paz -4:32:36 - LMT 1890 +-4:32:36 - CMT 1931 O 15 +-4:32:36 1 BST 1932 Mar 21 +-4 - -04 +R B 1931 o - O 3 11 1 - +R B 1932 1933 - Ap 1 0 0 - +R B 1932 o - O 3 0 1 - +R B 1949 1952 - D 1 0 1 - +R B 1950 o - Ap 16 1 0 - +R B 1951 1952 - Ap 1 0 0 - +R B 1953 o - Mar 1 0 0 - +R B 1963 o - D 9 0 1 - +R B 1964 o - Mar 1 0 0 - +R B 1965 o - Ja 31 0 1 - +R B 1965 o - Mar 31 0 0 - +R B 1965 o - D 1 0 1 - +R B 1966 1968 - Mar 1 0 0 - +R B 1966 1967 - N 1 0 1 - +R B 1985 o - N 2 0 1 - +R B 1986 o - Mar 15 0 0 - +R B 1986 o - O 25 0 1 - +R B 1987 o - F 14 0 0 - +R B 1987 o - O 25 0 1 - +R B 1988 o - F 7 0 0 - +R B 1988 o - O 16 0 1 - +R B 1989 o - Ja 29 0 0 - +R B 1989 o - O 15 0 1 - +R B 1990 o - F 11 0 0 - +R B 1990 o - O 21 0 1 - +R B 1991 o - F 17 0 0 - +R B 1991 o - O 20 0 1 - +R B 1992 o - F 9 0 0 - +R B 1992 o - O 25 0 1 - +R B 1993 o - Ja 31 0 0 - +R B 1993 1995 - O Sun>=11 0 1 - +R B 1994 1995 - F Sun>=15 0 0 - +R B 1996 o - F 11 0 0 - +R B 1996 o - O 6 0 1 - +R B 1997 o - F 16 0 0 - +R B 1997 o - O 6 0 1 - +R B 1998 o - Mar 1 0 0 - +R B 1998 o - O 11 0 1 - +R B 1999 o - F 21 0 0 - +R B 1999 o - O 3 0 1 - +R B 2000 o - F 27 0 0 - +R B 2000 2001 - O Sun>=8 0 1 - +R B 2001 2006 - F Sun>=15 0 0 - +R B 2002 o - N 3 0 1 - +R B 2003 o - O 19 0 1 - +R B 2004 o - N 2 0 1 - +R B 2005 o - O 16 0 1 - +R B 2006 o - N 5 0 1 - +R B 2007 o - F 25 0 0 - +R B 2007 o - O Sun>=8 0 1 - +R B 2008 2017 - O Sun>=15 0 1 - +R B 2008 2011 - F Sun>=15 0 0 - +R B 2012 o - F Sun>=22 0 0 - +R B 2013 2014 - F Sun>=15 0 0 - +R B 2015 o - F Sun>=22 0 0 - +R B 2016 2022 - F Sun>=15 0 0 - +R B 2018 ma - N Sun>=1 0 1 - +R B 2023 o - F Sun>=22 0 0 - +R B 2024 2025 - F Sun>=15 0 0 - +R B 2026 o - F Sun>=22 0 0 - +R B 2027 2033 - F Sun>=15 0 0 - +R B 2034 o - F Sun>=22 0 0 - +R B 2035 2036 - F Sun>=15 0 0 - +R B 2037 o - F Sun>=22 0 0 - +R B 2038 ma - F Sun>=15 0 0 - +Z America/Noronha -2:9:40 - LMT 1914 +-2 B -02/-01 1990 S 17 +-2 - -02 1999 S 30 +-2 B -02/-01 2000 O 15 +-2 - -02 2001 S 13 +-2 B -02/-01 2002 O +-2 - -02 +Z America/Belem -3:13:56 - LMT 1914 +-3 B -03/-02 1988 S 12 +-3 - -03 +Z America/Santarem -3:38:48 - LMT 1914 +-4 B -04/-03 1988 S 12 +-4 - -04 2008 Jun 24 +-3 - -03 +Z America/Fortaleza -2:34 - LMT 1914 +-3 B -03/-02 1990 S 17 +-3 - -03 1999 S 30 +-3 B -03/-02 2000 O 22 +-3 - -03 2001 S 13 +-3 B -03/-02 2002 O +-3 - -03 +Z America/Recife -2:19:36 - LMT 1914 +-3 B -03/-02 1990 S 17 +-3 - -03 1999 S 30 +-3 B -03/-02 2000 O 15 +-3 - -03 2001 S 13 +-3 B -03/-02 2002 O +-3 - -03 +Z America/Araguaina -3:12:48 - LMT 1914 +-3 B -03/-02 1990 S 17 +-3 - -03 1995 S 14 +-3 B -03/-02 2003 S 24 +-3 - -03 2012 O 21 +-3 B -03/-02 2013 S +-3 - -03 +Z America/Maceio -2:22:52 - LMT 1914 +-3 B -03/-02 1990 S 17 +-3 - -03 1995 O 13 +-3 B -03/-02 1996 S 4 +-3 - -03 1999 S 30 +-3 B -03/-02 2000 O 22 +-3 - -03 2001 S 13 +-3 B -03/-02 2002 O +-3 - -03 +Z America/Bahia -2:34:4 - LMT 1914 +-3 B -03/-02 2003 S 24 +-3 - -03 2011 O 16 +-3 B -03/-02 2012 O 21 +-3 - -03 +Z America/Sao_Paulo -3:6:28 - LMT 1914 +-3 B -03/-02 1963 O 23 +-3 1 -02 1964 +-3 B -03/-02 +Z America/Campo_Grande -3:38:28 - LMT 1914 +-4 B -04/-03 +Z America/Cuiaba -3:44:20 - LMT 1914 +-4 B -04/-03 2003 S 24 +-4 - -04 2004 O +-4 B -04/-03 +Z America/Porto_Velho -4:15:36 - LMT 1914 +-4 B -04/-03 1988 S 12 +-4 - -04 +Z America/Boa_Vista -4:2:40 - LMT 1914 +-4 B -04/-03 1988 S 12 +-4 - -04 1999 S 30 +-4 B -04/-03 2000 O 15 +-4 - -04 +Z America/Manaus -4:0:4 - LMT 1914 +-4 B -04/-03 1988 S 12 +-4 - -04 1993 S 28 +-4 B -04/-03 1994 S 22 +-4 - -04 +Z America/Eirunepe -4:39:28 - LMT 1914 +-5 B -05/-04 1988 S 12 +-5 - -05 1993 S 28 +-5 B -05/-04 1994 S 22 +-5 - -05 2008 Jun 24 +-4 - -04 2013 N 10 +-5 - -05 +Z America/Rio_Branco -4:31:12 - LMT 1914 +-5 B -05/-04 1988 S 12 +-5 - -05 2008 Jun 24 +-4 - -04 2013 N 10 +-5 - -05 +R x 1927 1931 - S 1 0 1 - +R x 1928 1932 - Ap 1 0 0 - +R x 1968 o - N 3 4u 1 - +R x 1969 o - Mar 30 3u 0 - +R x 1969 o - N 23 4u 1 - +R x 1970 o - Mar 29 3u 0 - +R x 1971 o - Mar 14 3u 0 - +R x 1970 1972 - O Sun>=9 4u 1 - +R x 1972 1986 - Mar Sun>=9 3u 0 - +R x 1973 o - S 30 4u 1 - +R x 1974 1987 - O Sun>=9 4u 1 - +R x 1987 o - Ap 12 3u 0 - +R x 1988 1990 - Mar Sun>=9 3u 0 - +R x 1988 1989 - O Sun>=9 4u 1 - +R x 1990 o - S 16 4u 1 - +R x 1991 1996 - Mar Sun>=9 3u 0 - +R x 1991 1997 - O Sun>=9 4u 1 - +R x 1997 o - Mar 30 3u 0 - +R x 1998 o - Mar Sun>=9 3u 0 - +R x 1998 o - S 27 4u 1 - +R x 1999 o - Ap 4 3u 0 - +R x 1999 2010 - O Sun>=9 4u 1 - +R x 2000 2007 - Mar Sun>=9 3u 0 - +R x 2008 o - Mar 30 3u 0 - +R x 2009 o - Mar Sun>=9 3u 0 - +R x 2010 o - Ap Sun>=1 3u 0 - +R x 2011 o - May Sun>=2 3u 0 - +R x 2011 o - Au Sun>=16 4u 1 - +R x 2012 2014 - Ap Sun>=23 3u 0 - +R x 2012 2014 - S Sun>=2 4u 1 - +R x 2016 2018 - May Sun>=9 3u 0 - +R x 2016 2018 - Au Sun>=9 4u 1 - +R x 2019 ma - Ap Sun>=2 3u 0 - +R x 2019 ma - S Sun>=2 4u 1 - +Z America/Santiago -4:42:46 - LMT 1890 +-4:42:46 - SMT 1910 Ja 10 +-5 - -05 1916 Jul +-4:42:46 - SMT 1918 S 10 +-4 - -04 1919 Jul +-4:42:46 - SMT 1927 S +-5 x -05/-04 1932 S +-4 - -04 1942 Jun +-5 - -05 1942 Au +-4 - -04 1946 Jul 15 +-4 1 -03 1946 S +-4 - -04 1947 Ap +-5 - -05 1947 May 21 23 +-4 x -04/-03 +Z America/Punta_Arenas -4:43:40 - LMT 1890 +-4:42:46 - SMT 1910 Ja 10 +-5 - -05 1916 Jul +-4:42:46 - SMT 1918 S 10 +-4 - -04 1919 Jul +-4:42:46 - SMT 1927 S +-5 x -05/-04 1932 S +-4 - -04 1942 Jun +-5 - -05 1942 Au +-4 - -04 1947 Ap +-5 - -05 1947 May 21 23 +-4 x -04/-03 2016 D 4 +-3 - -03 +Z Pacific/Easter -7:17:28 - LMT 1890 +-7:17:28 - EMT 1932 S +-7 x -07/-06 1982 Mar 14 3u +-6 x -06/-05 +Z Antarctica/Palmer 0 - -00 1965 +-4 A -04/-03 1969 O 5 +-3 A -03/-02 1982 May +-4 x -04/-03 2016 D 4 +-3 - -03 +R CO 1992 o - May 3 0 1 - +R CO 1993 o - Ap 4 0 0 - +Z America/Bogota -4:56:16 - LMT 1884 Mar 13 +-4:56:16 - BMT 1914 N 23 +-5 CO -05/-04 +Z America/Curacao -4:35:47 - LMT 1912 F 12 +-4:30 - -0430 1965 +-4 - AST +Li America/Curacao America/Lower_Princes +Li America/Curacao America/Kralendijk +R EC 1992 o - N 28 0 1 - +R EC 1993 o - F 5 0 0 - +Z America/Guayaquil -5:19:20 - LMT 1890 +-5:14 - QMT 1931 +-5 EC -05/-04 +Z Pacific/Galapagos -5:58:24 - LMT 1931 +-5 - -05 1986 +-6 EC -06/-05 +R FK 1937 1938 - S lastSun 0 1 - +R FK 1938 1942 - Mar Sun>=19 0 0 - +R FK 1939 o - O 1 0 1 - +R FK 1940 1942 - S lastSun 0 1 - +R FK 1943 o - Ja 1 0 0 - +R FK 1983 o - S lastSun 0 1 - +R FK 1984 1985 - Ap lastSun 0 0 - +R FK 1984 o - S 16 0 1 - +R FK 1985 2000 - S Sun>=9 0 1 - +R FK 1986 2000 - Ap Sun>=16 0 0 - +R FK 2001 2010 - Ap Sun>=15 2 0 - +R FK 2001 2010 - S Sun>=1 2 1 - +Z Atlantic/Stanley -3:51:24 - LMT 1890 +-3:51:24 - SMT 1912 Mar 12 +-4 FK -04/-03 1983 May +-3 FK -03/-02 1985 S 15 +-4 FK -04/-03 2010 S 5 2 +-3 - -03 +Z America/Cayenne -3:29:20 - LMT 1911 Jul +-4 - -04 1967 O +-3 - -03 +Z America/Guyana -3:52:40 - LMT 1915 Mar +-3:45 - -0345 1975 Jul 31 +-3 - -03 1991 +-4 - -04 +R y 1975 1988 - O 1 0 1 - +R y 1975 1978 - Mar 1 0 0 - +R y 1979 1991 - Ap 1 0 0 - +R y 1989 o - O 22 0 1 - +R y 1990 o - O 1 0 1 - +R y 1991 o - O 6 0 1 - +R y 1992 o - Mar 1 0 0 - +R y 1992 o - O 5 0 1 - +R y 1993 o - Mar 31 0 0 - +R y 1993 1995 - O 1 0 1 - +R y 1994 1995 - F lastSun 0 0 - +R y 1996 o - Mar 1 0 0 - +R y 1996 2001 - O Sun>=1 0 1 - +R y 1997 o - F lastSun 0 0 - +R y 1998 2001 - Mar Sun>=1 0 0 - +R y 2002 2004 - Ap Sun>=1 0 0 - +R y 2002 2003 - S Sun>=1 0 1 - +R y 2004 2009 - O Sun>=15 0 1 - +R y 2005 2009 - Mar Sun>=8 0 0 - +R y 2010 ma - O Sun>=1 0 1 - +R y 2010 2012 - Ap Sun>=8 0 0 - +R y 2013 ma - Mar Sun>=22 0 0 - +Z America/Asuncion -3:50:40 - LMT 1890 +-3:50:40 - AMT 1931 O 10 +-4 - -04 1972 O +-3 - -03 1974 Ap +-4 y -04/-03 +R PE 1938 o - Ja 1 0 1 - +R PE 1938 o - Ap 1 0 0 - +R PE 1938 1939 - S lastSun 0 1 - +R PE 1939 1940 - Mar Sun>=24 0 0 - +R PE 1986 1987 - Ja 1 0 1 - +R PE 1986 1987 - Ap 1 0 0 - +R PE 1990 o - Ja 1 0 1 - +R PE 1990 o - Ap 1 0 0 - +R PE 1994 o - Ja 1 0 1 - +R PE 1994 o - Ap 1 0 0 - +Z America/Lima -5:8:12 - LMT 1890 +-5:8:36 - LMT 1908 Jul 28 +-5 PE -05/-04 +Z Atlantic/South_Georgia -2:26:8 - LMT 1890 +-2 - -02 +Z America/Paramaribo -3:40:40 - LMT 1911 +-3:40:52 - PMT 1935 +-3:40:36 - PMT 1945 O +-3:30 - -0330 1984 O +-3 - -03 +Z America/Port_of_Spain -4:6:4 - LMT 1912 Mar 2 +-4 - AST +Li America/Port_of_Spain America/Anguilla +Li America/Port_of_Spain America/Antigua +Li America/Port_of_Spain America/Dominica +Li America/Port_of_Spain America/Grenada +Li America/Port_of_Spain America/Guadeloupe +Li America/Port_of_Spain America/Marigot +Li America/Port_of_Spain America/Montserrat +Li America/Port_of_Spain America/St_Barthelemy +Li America/Port_of_Spain America/St_Kitts +Li America/Port_of_Spain America/St_Lucia +Li America/Port_of_Spain America/St_Thomas +Li America/Port_of_Spain America/St_Vincent +Li America/Port_of_Spain America/Tortola +R U 1923 1925 - O 1 0 0:30 - +R U 1924 1926 - Ap 1 0 0 - +R U 1933 1938 - O lastSun 0 0:30 - +R U 1934 1941 - Mar lastSat 24 0 - +R U 1939 o - O 1 0 0:30 - +R U 1940 o - O 27 0 0:30 - +R U 1941 o - Au 1 0 0:30 - +R U 1942 o - D 14 0 0:30 - +R U 1943 o - Mar 14 0 0 - +R U 1959 o - May 24 0 0:30 - +R U 1959 o - N 15 0 0 - +R U 1960 o - Ja 17 0 1 - +R U 1960 o - Mar 6 0 0 - +R U 1965 o - Ap 4 0 1 - +R U 1965 o - S 26 0 0 - +R U 1968 o - May 27 0 0:30 - +R U 1968 o - D 1 0 0 - +R U 1970 o - Ap 25 0 1 - +R U 1970 o - Jun 14 0 0 - +R U 1972 o - Ap 23 0 1 - +R U 1972 o - Jul 16 0 0 - +R U 1974 o - Ja 13 0 1:30 - +R U 1974 o - Mar 10 0 0:30 - +R U 1974 o - S 1 0 0 - +R U 1974 o - D 22 0 1 - +R U 1975 o - Mar 30 0 0 - +R U 1976 o - D 19 0 1 - +R U 1977 o - Mar 6 0 0 - +R U 1977 o - D 4 0 1 - +R U 1978 1979 - Mar Sun>=1 0 0 - +R U 1978 o - D 17 0 1 - +R U 1979 o - Ap 29 0 1 - +R U 1980 o - Mar 16 0 0 - +R U 1987 o - D 14 0 1 - +R U 1988 o - F 28 0 0 - +R U 1988 o - D 11 0 1 - +R U 1989 o - Mar 5 0 0 - +R U 1989 o - O 29 0 1 - +R U 1990 o - F 25 0 0 - +R U 1990 1991 - O Sun>=21 0 1 - +R U 1991 1992 - Mar Sun>=1 0 0 - +R U 1992 o - O 18 0 1 - +R U 1993 o - F 28 0 0 - +R U 2004 o - S 19 0 1 - +R U 2005 o - Mar 27 2 0 - +R U 2005 o - O 9 2 1 - +R U 2006 2015 - Mar Sun>=8 2 0 - +R U 2006 2014 - O Sun>=1 2 1 - +Z America/Montevideo -3:44:51 - LMT 1908 Jun 10 +-3:44:51 - MMT 1920 May +-4 - -04 1923 O +-3:30 U -0330/-03 1942 D 14 +-3 U -03/-0230 1960 +-3 U -03/-02 1968 +-3 U -03/-0230 1970 +-3 U -03/-02 1974 +-3 U -03/-0130 1974 Mar 10 +-3 U -03/-0230 1974 D 22 +-3 U -03/-02 +Z America/Caracas -4:27:44 - LMT 1890 +-4:27:40 - CMT 1912 F 12 +-4:30 - -0430 1965 +-4 - -04 2007 D 9 3 +-4:30 - -0430 2016 May 1 2:30 +-4 - -04 +Z Etc/GMT 0 - GMT +Z Etc/UTC 0 - UTC +Z Etc/UCT 0 - UCT +Li Etc/GMT GMT +Li Etc/UTC Etc/Universal +Li Etc/UTC Etc/Zulu +Li Etc/GMT Etc/Greenwich +Li Etc/GMT Etc/GMT-0 +Li Etc/GMT Etc/GMT+0 +Li Etc/GMT Etc/GMT0 +Z Etc/GMT-14 14 - +14 +Z Etc/GMT-13 13 - +13 +Z Etc/GMT-12 12 - +12 +Z Etc/GMT-11 11 - +11 +Z Etc/GMT-10 10 - +10 +Z Etc/GMT-9 9 - +09 +Z Etc/GMT-8 8 - +08 +Z Etc/GMT-7 7 - +07 +Z Etc/GMT-6 6 - +06 +Z Etc/GMT-5 5 - +05 +Z Etc/GMT-4 4 - +04 +Z Etc/GMT-3 3 - +03 +Z Etc/GMT-2 2 - +02 +Z Etc/GMT-1 1 - +01 +Z Etc/GMT+1 -1 - -01 +Z Etc/GMT+2 -2 - -02 +Z Etc/GMT+3 -3 - -03 +Z Etc/GMT+4 -4 - -04 +Z Etc/GMT+5 -5 - -05 +Z Etc/GMT+6 -6 - -06 +Z Etc/GMT+7 -7 - -07 +Z Etc/GMT+8 -8 - -08 +Z Etc/GMT+9 -9 - -09 +Z Etc/GMT+10 -10 - -10 +Z Etc/GMT+11 -11 - -11 +Z Etc/GMT+12 -12 - -12 +Z Factory 0 - -00 +Li Africa/Nairobi Africa/Asmera +Li Africa/Abidjan Africa/Timbuktu +Li America/Argentina/Catamarca America/Argentina/ComodRivadavia +Li America/Adak America/Atka +Li America/Argentina/Buenos_Aires America/Buenos_Aires +Li America/Argentina/Catamarca America/Catamarca +Li America/Atikokan America/Coral_Harbour +Li America/Argentina/Cordoba America/Cordoba +Li America/Tijuana America/Ensenada +Li America/Indiana/Indianapolis America/Fort_Wayne +Li America/Indiana/Indianapolis America/Indianapolis +Li America/Argentina/Jujuy America/Jujuy +Li America/Indiana/Knox America/Knox_IN +Li America/Kentucky/Louisville America/Louisville +Li America/Argentina/Mendoza America/Mendoza +Li America/Toronto America/Montreal +Li America/Rio_Branco America/Porto_Acre +Li America/Argentina/Cordoba America/Rosario +Li America/Tijuana America/Santa_Isabel +Li America/Denver America/Shiprock +Li America/Port_of_Spain America/Virgin +Li Pacific/Auckland Antarctica/South_Pole +Li Asia/Ashgabat Asia/Ashkhabad +Li Asia/Kolkata Asia/Calcutta +Li Asia/Shanghai Asia/Chongqing +Li Asia/Shanghai Asia/Chungking +Li Asia/Dhaka Asia/Dacca +Li Asia/Shanghai Asia/Harbin +Li Asia/Urumqi Asia/Kashgar +Li Asia/Kathmandu Asia/Katmandu +Li Asia/Macau Asia/Macao +Li Asia/Yangon Asia/Rangoon +Li Asia/Ho_Chi_Minh Asia/Saigon +Li Asia/Jerusalem Asia/Tel_Aviv +Li Asia/Thimphu Asia/Thimbu +Li Asia/Makassar Asia/Ujung_Pandang +Li Asia/Ulaanbaatar Asia/Ulan_Bator +Li Atlantic/Faroe Atlantic/Faeroe +Li Europe/Oslo Atlantic/Jan_Mayen +Li Australia/Sydney Australia/ACT +Li Australia/Sydney Australia/Canberra +Li Australia/Lord_Howe Australia/LHI +Li Australia/Sydney Australia/NSW +Li Australia/Darwin Australia/North +Li Australia/Brisbane Australia/Queensland +Li Australia/Adelaide Australia/South +Li Australia/Hobart Australia/Tasmania +Li Australia/Melbourne Australia/Victoria +Li Australia/Perth Australia/West +Li Australia/Broken_Hill Australia/Yancowinna +Li America/Rio_Branco Brazil/Acre +Li America/Noronha Brazil/DeNoronha +Li America/Sao_Paulo Brazil/East +Li America/Manaus Brazil/West +Li America/Halifax Canada/Atlantic +Li America/Winnipeg Canada/Central +Li America/Toronto Canada/Eastern +Li America/Edmonton Canada/Mountain +Li America/St_Johns Canada/Newfoundland +Li America/Vancouver Canada/Pacific +Li America/Regina Canada/Saskatchewan +Li America/Whitehorse Canada/Yukon +Li America/Santiago Chile/Continental +Li Pacific/Easter Chile/EasterIsland +Li America/Havana Cuba +Li Africa/Cairo Egypt +Li Europe/Dublin Eire +Li Europe/London Europe/Belfast +Li Europe/Chisinau Europe/Tiraspol +Li Europe/London GB +Li Europe/London GB-Eire +Li Etc/GMT GMT+0 +Li Etc/GMT GMT-0 +Li Etc/GMT GMT0 +Li Etc/GMT Greenwich +Li Asia/Hong_Kong Hongkong +Li Atlantic/Reykjavik Iceland +Li Asia/Tehran Iran +Li Asia/Jerusalem Israel +Li America/Jamaica Jamaica +Li Asia/Tokyo Japan +Li Pacific/Kwajalein Kwajalein +Li Africa/Tripoli Libya +Li America/Tijuana Mexico/BajaNorte +Li America/Mazatlan Mexico/BajaSur +Li America/Mexico_City Mexico/General +Li Pacific/Auckland NZ +Li Pacific/Chatham NZ-CHAT +Li America/Denver Navajo +Li Asia/Shanghai PRC +Li Pacific/Honolulu Pacific/Johnston +Li Pacific/Pohnpei Pacific/Ponape +Li Pacific/Pago_Pago Pacific/Samoa +Li Pacific/Chuuk Pacific/Truk +Li Pacific/Chuuk Pacific/Yap +Li Europe/Warsaw Poland +Li Europe/Lisbon Portugal +Li Asia/Taipei ROC +Li Asia/Seoul ROK +Li Asia/Singapore Singapore +Li Europe/Istanbul Turkey +Li Etc/UCT UCT +Li America/Anchorage US/Alaska +Li America/Adak US/Aleutian +Li America/Phoenix US/Arizona +Li America/Chicago US/Central +Li America/Indiana/Indianapolis US/East-Indiana +Li America/New_York US/Eastern +Li Pacific/Honolulu US/Hawaii +Li America/Indiana/Knox US/Indiana-Starke +Li America/Detroit US/Michigan +Li America/Denver US/Mountain +Li America/Los_Angeles US/Pacific +Li Pacific/Pago_Pago US/Samoa +Li Etc/UTC UTC +Li Etc/UTC Universal +Li Europe/Moscow W-SU +Li Etc/UTC Zulu diff --git a/libs/pytz/zoneinfo/zone.tab b/libs/pytz/zoneinfo/zone.tab new file mode 100644 index 00000000..dcb6e1da --- /dev/null +++ b/libs/pytz/zoneinfo/zone.tab @@ -0,0 +1,448 @@ +# tzdb timezone descriptions (deprecated version) +# +# This file is in the public domain, so clarified as of +# 2009-05-17 by Arthur David Olson. +# +# From Paul Eggert (2018-06-27): +# This file is intended as a backward-compatibility aid for older programs. +# New programs should use zone1970.tab. This file is like zone1970.tab (see +# zone1970.tab's comments), but with the following additional restrictions: +# +# 1. This file contains only ASCII characters. +# 2. The first data column contains exactly one country code. +# +# Because of (2), each row stands for an area that is the intersection +# of a region identified by a country code and of a timezone where civil +# clocks have agreed since 1970; this is a narrower definition than +# that of zone1970.tab. +# +# This table is intended as an aid for users, to help them select timezones +# appropriate for their practical needs. It is not intended to take or +# endorse any position on legal or territorial claims. +# +#country- +#code coordinates TZ comments +AD +4230+00131 Europe/Andorra +AE +2518+05518 Asia/Dubai +AF +3431+06912 Asia/Kabul +AG +1703-06148 America/Antigua +AI +1812-06304 America/Anguilla +AL +4120+01950 Europe/Tirane +AM +4011+04430 Asia/Yerevan +AO -0848+01314 Africa/Luanda +AQ -7750+16636 Antarctica/McMurdo New Zealand time - McMurdo, South Pole +AQ -6617+11031 Antarctica/Casey Casey +AQ -6835+07758 Antarctica/Davis Davis +AQ -6640+14001 Antarctica/DumontDUrville Dumont-d'Urville +AQ -6736+06253 Antarctica/Mawson Mawson +AQ -6448-06406 Antarctica/Palmer Palmer +AQ -6734-06808 Antarctica/Rothera Rothera +AQ -690022+0393524 Antarctica/Syowa Syowa +AQ -720041+0023206 Antarctica/Troll Troll +AQ -7824+10654 Antarctica/Vostok Vostok +AR -3436-05827 America/Argentina/Buenos_Aires Buenos Aires (BA, CF) +AR -3124-06411 America/Argentina/Cordoba Argentina (most areas: CB, CC, CN, ER, FM, MN, SE, SF) +AR -2447-06525 America/Argentina/Salta Salta (SA, LP, NQ, RN) +AR -2411-06518 America/Argentina/Jujuy Jujuy (JY) +AR -2649-06513 America/Argentina/Tucuman Tucuman (TM) +AR -2828-06547 America/Argentina/Catamarca Catamarca (CT); Chubut (CH) +AR -2926-06651 America/Argentina/La_Rioja La Rioja (LR) +AR -3132-06831 America/Argentina/San_Juan San Juan (SJ) +AR -3253-06849 America/Argentina/Mendoza Mendoza (MZ) +AR -3319-06621 America/Argentina/San_Luis San Luis (SL) +AR -5138-06913 America/Argentina/Rio_Gallegos Santa Cruz (SC) +AR -5448-06818 America/Argentina/Ushuaia Tierra del Fuego (TF) +AS -1416-17042 Pacific/Pago_Pago +AT +4813+01620 Europe/Vienna +AU -3133+15905 Australia/Lord_Howe Lord Howe Island +AU -5430+15857 Antarctica/Macquarie Macquarie Island +AU -4253+14719 Australia/Hobart Tasmania (most areas) +AU -3956+14352 Australia/Currie Tasmania (King Island) +AU -3749+14458 Australia/Melbourne Victoria +AU -3352+15113 Australia/Sydney New South Wales (most areas) +AU -3157+14127 Australia/Broken_Hill New South Wales (Yancowinna) +AU -2728+15302 Australia/Brisbane Queensland (most areas) +AU -2016+14900 Australia/Lindeman Queensland (Whitsunday Islands) +AU -3455+13835 Australia/Adelaide South Australia +AU -1228+13050 Australia/Darwin Northern Territory +AU -3157+11551 Australia/Perth Western Australia (most areas) +AU -3143+12852 Australia/Eucla Western Australia (Eucla) +AW +1230-06958 America/Aruba +AX +6006+01957 Europe/Mariehamn +AZ +4023+04951 Asia/Baku +BA +4352+01825 Europe/Sarajevo +BB +1306-05937 America/Barbados +BD +2343+09025 Asia/Dhaka +BE +5050+00420 Europe/Brussels +BF +1222-00131 Africa/Ouagadougou +BG +4241+02319 Europe/Sofia +BH +2623+05035 Asia/Bahrain +BI -0323+02922 Africa/Bujumbura +BJ +0629+00237 Africa/Porto-Novo +BL +1753-06251 America/St_Barthelemy +BM +3217-06446 Atlantic/Bermuda +BN +0456+11455 Asia/Brunei +BO -1630-06809 America/La_Paz +BQ +120903-0681636 America/Kralendijk +BR -0351-03225 America/Noronha Atlantic islands +BR -0127-04829 America/Belem Para (east); Amapa +BR -0343-03830 America/Fortaleza Brazil (northeast: MA, PI, CE, RN, PB) +BR -0803-03454 America/Recife Pernambuco +BR -0712-04812 America/Araguaina Tocantins +BR -0940-03543 America/Maceio Alagoas, Sergipe +BR -1259-03831 America/Bahia Bahia +BR -2332-04637 America/Sao_Paulo Brazil (southeast: GO, DF, MG, ES, RJ, SP, PR, SC, RS) +BR -2027-05437 America/Campo_Grande Mato Grosso do Sul +BR -1535-05605 America/Cuiaba Mato Grosso +BR -0226-05452 America/Santarem Para (west) +BR -0846-06354 America/Porto_Velho Rondonia +BR +0249-06040 America/Boa_Vista Roraima +BR -0308-06001 America/Manaus Amazonas (east) +BR -0640-06952 America/Eirunepe Amazonas (west) +BR -0958-06748 America/Rio_Branco Acre +BS +2505-07721 America/Nassau +BT +2728+08939 Asia/Thimphu +BW -2439+02555 Africa/Gaborone +BY +5354+02734 Europe/Minsk +BZ +1730-08812 America/Belize +CA +4734-05243 America/St_Johns Newfoundland; Labrador (southeast) +CA +4439-06336 America/Halifax Atlantic - NS (most areas); PE +CA +4612-05957 America/Glace_Bay Atlantic - NS (Cape Breton) +CA +4606-06447 America/Moncton Atlantic - New Brunswick +CA +5320-06025 America/Goose_Bay Atlantic - Labrador (most areas) +CA +5125-05707 America/Blanc-Sablon AST - QC (Lower North Shore) +CA +4339-07923 America/Toronto Eastern - ON, QC (most areas) +CA +4901-08816 America/Nipigon Eastern - ON, QC (no DST 1967-73) +CA +4823-08915 America/Thunder_Bay Eastern - ON (Thunder Bay) +CA +6344-06828 America/Iqaluit Eastern - NU (most east areas) +CA +6608-06544 America/Pangnirtung Eastern - NU (Pangnirtung) +CA +484531-0913718 America/Atikokan EST - ON (Atikokan); NU (Coral H) +CA +4953-09709 America/Winnipeg Central - ON (west); Manitoba +CA +4843-09434 America/Rainy_River Central - ON (Rainy R, Ft Frances) +CA +744144-0944945 America/Resolute Central - NU (Resolute) +CA +624900-0920459 America/Rankin_Inlet Central - NU (central) +CA +5024-10439 America/Regina CST - SK (most areas) +CA +5017-10750 America/Swift_Current CST - SK (midwest) +CA +5333-11328 America/Edmonton Mountain - AB; BC (E); SK (W) +CA +690650-1050310 America/Cambridge_Bay Mountain - NU (west) +CA +6227-11421 America/Yellowknife Mountain - NT (central) +CA +682059-1334300 America/Inuvik Mountain - NT (west) +CA +4906-11631 America/Creston MST - BC (Creston) +CA +5946-12014 America/Dawson_Creek MST - BC (Dawson Cr, Ft St John) +CA +5848-12242 America/Fort_Nelson MST - BC (Ft Nelson) +CA +4916-12307 America/Vancouver Pacific - BC (most areas) +CA +6043-13503 America/Whitehorse Pacific - Yukon (south) +CA +6404-13925 America/Dawson Pacific - Yukon (north) +CC -1210+09655 Indian/Cocos +CD -0418+01518 Africa/Kinshasa Dem. Rep. of Congo (west) +CD -1140+02728 Africa/Lubumbashi Dem. Rep. of Congo (east) +CF +0422+01835 Africa/Bangui +CG -0416+01517 Africa/Brazzaville +CH +4723+00832 Europe/Zurich +CI +0519-00402 Africa/Abidjan +CK -2114-15946 Pacific/Rarotonga +CL -3327-07040 America/Santiago Chile (most areas) +CL -5309-07055 America/Punta_Arenas Region of Magallanes +CL -2709-10926 Pacific/Easter Easter Island +CM +0403+00942 Africa/Douala +CN +3114+12128 Asia/Shanghai Beijing Time +CN +4348+08735 Asia/Urumqi Xinjiang Time +CO +0436-07405 America/Bogota +CR +0956-08405 America/Costa_Rica +CU +2308-08222 America/Havana +CV +1455-02331 Atlantic/Cape_Verde +CW +1211-06900 America/Curacao +CX -1025+10543 Indian/Christmas +CY +3510+03322 Asia/Nicosia Cyprus (most areas) +CY +3507+03357 Asia/Famagusta Northern Cyprus +CZ +5005+01426 Europe/Prague +DE +5230+01322 Europe/Berlin Germany (most areas) +DE +4742+00841 Europe/Busingen Busingen +DJ +1136+04309 Africa/Djibouti +DK +5540+01235 Europe/Copenhagen +DM +1518-06124 America/Dominica +DO +1828-06954 America/Santo_Domingo +DZ +3647+00303 Africa/Algiers +EC -0210-07950 America/Guayaquil Ecuador (mainland) +EC -0054-08936 Pacific/Galapagos Galapagos Islands +EE +5925+02445 Europe/Tallinn +EG +3003+03115 Africa/Cairo +EH +2709-01312 Africa/El_Aaiun +ER +1520+03853 Africa/Asmara +ES +4024-00341 Europe/Madrid Spain (mainland) +ES +3553-00519 Africa/Ceuta Ceuta, Melilla +ES +2806-01524 Atlantic/Canary Canary Islands +ET +0902+03842 Africa/Addis_Ababa +FI +6010+02458 Europe/Helsinki +FJ -1808+17825 Pacific/Fiji +FK -5142-05751 Atlantic/Stanley +FM +0725+15147 Pacific/Chuuk Chuuk/Truk, Yap +FM +0658+15813 Pacific/Pohnpei Pohnpei/Ponape +FM +0519+16259 Pacific/Kosrae Kosrae +FO +6201-00646 Atlantic/Faroe +FR +4852+00220 Europe/Paris +GA +0023+00927 Africa/Libreville +GB +513030-0000731 Europe/London +GD +1203-06145 America/Grenada +GE +4143+04449 Asia/Tbilisi +GF +0456-05220 America/Cayenne +GG +492717-0023210 Europe/Guernsey +GH +0533-00013 Africa/Accra +GI +3608-00521 Europe/Gibraltar +GL +6411-05144 America/Godthab Greenland (most areas) +GL +7646-01840 America/Danmarkshavn National Park (east coast) +GL +7029-02158 America/Scoresbysund Scoresbysund/Ittoqqortoormiit +GL +7634-06847 America/Thule Thule/Pituffik +GM +1328-01639 Africa/Banjul +GN +0931-01343 Africa/Conakry +GP +1614-06132 America/Guadeloupe +GQ +0345+00847 Africa/Malabo +GR +3758+02343 Europe/Athens +GS -5416-03632 Atlantic/South_Georgia +GT +1438-09031 America/Guatemala +GU +1328+14445 Pacific/Guam +GW +1151-01535 Africa/Bissau +GY +0648-05810 America/Guyana +HK +2217+11409 Asia/Hong_Kong +HN +1406-08713 America/Tegucigalpa +HR +4548+01558 Europe/Zagreb +HT +1832-07220 America/Port-au-Prince +HU +4730+01905 Europe/Budapest +ID -0610+10648 Asia/Jakarta Java, Sumatra +ID -0002+10920 Asia/Pontianak Borneo (west, central) +ID -0507+11924 Asia/Makassar Borneo (east, south); Sulawesi/Celebes, Bali, Nusa Tengarra; Timor (west) +ID -0232+14042 Asia/Jayapura New Guinea (West Papua / Irian Jaya); Malukus/Moluccas +IE +5320-00615 Europe/Dublin +IL +314650+0351326 Asia/Jerusalem +IM +5409-00428 Europe/Isle_of_Man +IN +2232+08822 Asia/Kolkata +IO -0720+07225 Indian/Chagos +IQ +3321+04425 Asia/Baghdad +IR +3540+05126 Asia/Tehran +IS +6409-02151 Atlantic/Reykjavik +IT +4154+01229 Europe/Rome +JE +491101-0020624 Europe/Jersey +JM +175805-0764736 America/Jamaica +JO +3157+03556 Asia/Amman +JP +353916+1394441 Asia/Tokyo +KE -0117+03649 Africa/Nairobi +KG +4254+07436 Asia/Bishkek +KH +1133+10455 Asia/Phnom_Penh +KI +0125+17300 Pacific/Tarawa Gilbert Islands +KI -0308-17105 Pacific/Enderbury Phoenix Islands +KI +0152-15720 Pacific/Kiritimati Line Islands +KM -1141+04316 Indian/Comoro +KN +1718-06243 America/St_Kitts +KP +3901+12545 Asia/Pyongyang +KR +3733+12658 Asia/Seoul +KW +2920+04759 Asia/Kuwait +KY +1918-08123 America/Cayman +KZ +4315+07657 Asia/Almaty Kazakhstan (most areas) +KZ +4448+06528 Asia/Qyzylorda Qyzylorda/Kyzylorda/Kzyl-Orda +KZ +5017+05710 Asia/Aqtobe Aqtobe/Aktobe +KZ +4431+05016 Asia/Aqtau Mangghystau/Mankistau +KZ +4707+05156 Asia/Atyrau Atyrau/Atirau/Gur'yev +KZ +5113+05121 Asia/Oral West Kazakhstan +LA +1758+10236 Asia/Vientiane +LB +3353+03530 Asia/Beirut +LC +1401-06100 America/St_Lucia +LI +4709+00931 Europe/Vaduz +LK +0656+07951 Asia/Colombo +LR +0618-01047 Africa/Monrovia +LS -2928+02730 Africa/Maseru +LT +5441+02519 Europe/Vilnius +LU +4936+00609 Europe/Luxembourg +LV +5657+02406 Europe/Riga +LY +3254+01311 Africa/Tripoli +MA +3339-00735 Africa/Casablanca +MC +4342+00723 Europe/Monaco +MD +4700+02850 Europe/Chisinau +ME +4226+01916 Europe/Podgorica +MF +1804-06305 America/Marigot +MG -1855+04731 Indian/Antananarivo +MH +0709+17112 Pacific/Majuro Marshall Islands (most areas) +MH +0905+16720 Pacific/Kwajalein Kwajalein +MK +4159+02126 Europe/Skopje +ML +1239-00800 Africa/Bamako +MM +1647+09610 Asia/Yangon +MN +4755+10653 Asia/Ulaanbaatar Mongolia (most areas) +MN +4801+09139 Asia/Hovd Bayan-Olgiy, Govi-Altai, Hovd, Uvs, Zavkhan +MN +4804+11430 Asia/Choibalsan Dornod, Sukhbaatar +MO +221150+1133230 Asia/Macau +MP +1512+14545 Pacific/Saipan +MQ +1436-06105 America/Martinique +MR +1806-01557 Africa/Nouakchott +MS +1643-06213 America/Montserrat +MT +3554+01431 Europe/Malta +MU -2010+05730 Indian/Mauritius +MV +0410+07330 Indian/Maldives +MW -1547+03500 Africa/Blantyre +MX +1924-09909 America/Mexico_City Central Time +MX +2105-08646 America/Cancun Eastern Standard Time - Quintana Roo +MX +2058-08937 America/Merida Central Time - Campeche, Yucatan +MX +2540-10019 America/Monterrey Central Time - Durango; Coahuila, Nuevo Leon, Tamaulipas (most areas) +MX +2550-09730 America/Matamoros Central Time US - Coahuila, Nuevo Leon, Tamaulipas (US border) +MX +2313-10625 America/Mazatlan Mountain Time - Baja California Sur, Nayarit, Sinaloa +MX +2838-10605 America/Chihuahua Mountain Time - Chihuahua (most areas) +MX +2934-10425 America/Ojinaga Mountain Time US - Chihuahua (US border) +MX +2904-11058 America/Hermosillo Mountain Standard Time - Sonora +MX +3232-11701 America/Tijuana Pacific Time US - Baja California +MX +2048-10515 America/Bahia_Banderas Central Time - Bahia de Banderas +MY +0310+10142 Asia/Kuala_Lumpur Malaysia (peninsula) +MY +0133+11020 Asia/Kuching Sabah, Sarawak +MZ -2558+03235 Africa/Maputo +NA -2234+01706 Africa/Windhoek +NC -2216+16627 Pacific/Noumea +NE +1331+00207 Africa/Niamey +NF -2903+16758 Pacific/Norfolk +NG +0627+00324 Africa/Lagos +NI +1209-08617 America/Managua +NL +5222+00454 Europe/Amsterdam +NO +5955+01045 Europe/Oslo +NP +2743+08519 Asia/Kathmandu +NR -0031+16655 Pacific/Nauru +NU -1901-16955 Pacific/Niue +NZ -3652+17446 Pacific/Auckland New Zealand (most areas) +NZ -4357-17633 Pacific/Chatham Chatham Islands +OM +2336+05835 Asia/Muscat +PA +0858-07932 America/Panama +PE -1203-07703 America/Lima +PF -1732-14934 Pacific/Tahiti Society Islands +PF -0900-13930 Pacific/Marquesas Marquesas Islands +PF -2308-13457 Pacific/Gambier Gambier Islands +PG -0930+14710 Pacific/Port_Moresby Papua New Guinea (most areas) +PG -0613+15534 Pacific/Bougainville Bougainville +PH +1435+12100 Asia/Manila +PK +2452+06703 Asia/Karachi +PL +5215+02100 Europe/Warsaw +PM +4703-05620 America/Miquelon +PN -2504-13005 Pacific/Pitcairn +PR +182806-0660622 America/Puerto_Rico +PS +3130+03428 Asia/Gaza Gaza Strip +PS +313200+0350542 Asia/Hebron West Bank +PT +3843-00908 Europe/Lisbon Portugal (mainland) +PT +3238-01654 Atlantic/Madeira Madeira Islands +PT +3744-02540 Atlantic/Azores Azores +PW +0720+13429 Pacific/Palau +PY -2516-05740 America/Asuncion +QA +2517+05132 Asia/Qatar +RE -2052+05528 Indian/Reunion +RO +4426+02606 Europe/Bucharest +RS +4450+02030 Europe/Belgrade +RU +5443+02030 Europe/Kaliningrad MSK-01 - Kaliningrad +RU +554521+0373704 Europe/Moscow MSK+00 - Moscow area +RU +4457+03406 Europe/Simferopol MSK+00 - Crimea +RU +4844+04425 Europe/Volgograd MSK+00 - Volgograd +RU +5836+04939 Europe/Kirov MSK+00 - Kirov +RU +4621+04803 Europe/Astrakhan MSK+01 - Astrakhan +RU +5134+04602 Europe/Saratov MSK+01 - Saratov +RU +5420+04824 Europe/Ulyanovsk MSK+01 - Ulyanovsk +RU +5312+05009 Europe/Samara MSK+01 - Samara, Udmurtia +RU +5651+06036 Asia/Yekaterinburg MSK+02 - Urals +RU +5500+07324 Asia/Omsk MSK+03 - Omsk +RU +5502+08255 Asia/Novosibirsk MSK+04 - Novosibirsk +RU +5322+08345 Asia/Barnaul MSK+04 - Altai +RU +5630+08458 Asia/Tomsk MSK+04 - Tomsk +RU +5345+08707 Asia/Novokuznetsk MSK+04 - Kemerovo +RU +5601+09250 Asia/Krasnoyarsk MSK+04 - Krasnoyarsk area +RU +5216+10420 Asia/Irkutsk MSK+05 - Irkutsk, Buryatia +RU +5203+11328 Asia/Chita MSK+06 - Zabaykalsky +RU +6200+12940 Asia/Yakutsk MSK+06 - Lena River +RU +623923+1353314 Asia/Khandyga MSK+06 - Tomponsky, Ust-Maysky +RU +4310+13156 Asia/Vladivostok MSK+07 - Amur River +RU +643337+1431336 Asia/Ust-Nera MSK+07 - Oymyakonsky +RU +5934+15048 Asia/Magadan MSK+08 - Magadan +RU +4658+14242 Asia/Sakhalin MSK+08 - Sakhalin Island +RU +6728+15343 Asia/Srednekolymsk MSK+08 - Sakha (E); North Kuril Is +RU +5301+15839 Asia/Kamchatka MSK+09 - Kamchatka +RU +6445+17729 Asia/Anadyr MSK+09 - Bering Sea +RW -0157+03004 Africa/Kigali +SA +2438+04643 Asia/Riyadh +SB -0932+16012 Pacific/Guadalcanal +SC -0440+05528 Indian/Mahe +SD +1536+03232 Africa/Khartoum +SE +5920+01803 Europe/Stockholm +SG +0117+10351 Asia/Singapore +SH -1555-00542 Atlantic/St_Helena +SI +4603+01431 Europe/Ljubljana +SJ +7800+01600 Arctic/Longyearbyen +SK +4809+01707 Europe/Bratislava +SL +0830-01315 Africa/Freetown +SM +4355+01228 Europe/San_Marino +SN +1440-01726 Africa/Dakar +SO +0204+04522 Africa/Mogadishu +SR +0550-05510 America/Paramaribo +SS +0451+03137 Africa/Juba +ST +0020+00644 Africa/Sao_Tome +SV +1342-08912 America/El_Salvador +SX +180305-0630250 America/Lower_Princes +SY +3330+03618 Asia/Damascus +SZ -2618+03106 Africa/Mbabane +TC +2128-07108 America/Grand_Turk +TD +1207+01503 Africa/Ndjamena +TF -492110+0701303 Indian/Kerguelen +TG +0608+00113 Africa/Lome +TH +1345+10031 Asia/Bangkok +TJ +3835+06848 Asia/Dushanbe +TK -0922-17114 Pacific/Fakaofo +TL -0833+12535 Asia/Dili +TM +3757+05823 Asia/Ashgabat +TN +3648+01011 Africa/Tunis +TO -2110-17510 Pacific/Tongatapu +TR +4101+02858 Europe/Istanbul +TT +1039-06131 America/Port_of_Spain +TV -0831+17913 Pacific/Funafuti +TW +2503+12130 Asia/Taipei +TZ -0648+03917 Africa/Dar_es_Salaam +UA +5026+03031 Europe/Kiev Ukraine (most areas) +UA +4837+02218 Europe/Uzhgorod Ruthenia +UA +4750+03510 Europe/Zaporozhye Zaporozh'ye/Zaporizhia; Lugansk/Luhansk (east) +UG +0019+03225 Africa/Kampala +UM +2813-17722 Pacific/Midway Midway Islands +UM +1917+16637 Pacific/Wake Wake Island +US +404251-0740023 America/New_York Eastern (most areas) +US +421953-0830245 America/Detroit Eastern - MI (most areas) +US +381515-0854534 America/Kentucky/Louisville Eastern - KY (Louisville area) +US +364947-0845057 America/Kentucky/Monticello Eastern - KY (Wayne) +US +394606-0860929 America/Indiana/Indianapolis Eastern - IN (most areas) +US +384038-0873143 America/Indiana/Vincennes Eastern - IN (Da, Du, K, Mn) +US +410305-0863611 America/Indiana/Winamac Eastern - IN (Pulaski) +US +382232-0862041 America/Indiana/Marengo Eastern - IN (Crawford) +US +382931-0871643 America/Indiana/Petersburg Eastern - IN (Pike) +US +384452-0850402 America/Indiana/Vevay Eastern - IN (Switzerland) +US +415100-0873900 America/Chicago Central (most areas) +US +375711-0864541 America/Indiana/Tell_City Central - IN (Perry) +US +411745-0863730 America/Indiana/Knox Central - IN (Starke) +US +450628-0873651 America/Menominee Central - MI (Wisconsin border) +US +470659-1011757 America/North_Dakota/Center Central - ND (Oliver) +US +465042-1012439 America/North_Dakota/New_Salem Central - ND (Morton rural) +US +471551-1014640 America/North_Dakota/Beulah Central - ND (Mercer) +US +394421-1045903 America/Denver Mountain (most areas) +US +433649-1161209 America/Boise Mountain - ID (south); OR (east) +US +332654-1120424 America/Phoenix MST - Arizona (except Navajo) +US +340308-1181434 America/Los_Angeles Pacific +US +611305-1495401 America/Anchorage Alaska (most areas) +US +581807-1342511 America/Juneau Alaska - Juneau area +US +571035-1351807 America/Sitka Alaska - Sitka area +US +550737-1313435 America/Metlakatla Alaska - Annette Island +US +593249-1394338 America/Yakutat Alaska - Yakutat +US +643004-1652423 America/Nome Alaska (west) +US +515248-1763929 America/Adak Aleutian Islands +US +211825-1575130 Pacific/Honolulu Hawaii +UY -345433-0561245 America/Montevideo +UZ +3940+06648 Asia/Samarkand Uzbekistan (west) +UZ +4120+06918 Asia/Tashkent Uzbekistan (east) +VA +415408+0122711 Europe/Vatican +VC +1309-06114 America/St_Vincent +VE +1030-06656 America/Caracas +VG +1827-06437 America/Tortola +VI +1821-06456 America/St_Thomas +VN +1045+10640 Asia/Ho_Chi_Minh +VU -1740+16825 Pacific/Efate +WF -1318-17610 Pacific/Wallis +WS -1350-17144 Pacific/Apia +YE +1245+04512 Asia/Aden +YT -1247+04514 Indian/Mayotte +ZA -2615+02800 Africa/Johannesburg +ZM -1525+02817 Africa/Lusaka +ZW -1750+03103 Africa/Harare diff --git a/libs/pytz/zoneinfo/zone1970.tab b/libs/pytz/zoneinfo/zone1970.tab new file mode 100644 index 00000000..7c86fb69 --- /dev/null +++ b/libs/pytz/zoneinfo/zone1970.tab @@ -0,0 +1,382 @@ +# tzdb timezone descriptions +# +# This file is in the public domain. +# +# From Paul Eggert (2018-06-27): +# This file contains a table where each row stands for a timezone where +# civil timestamps have agreed since 1970. Columns are separated by +# a single tab. Lines beginning with '#' are comments. All text uses +# UTF-8 encoding. The columns of the table are as follows: +# +# 1. The countries that overlap the timezone, as a comma-separated list +# of ISO 3166 2-character country codes. See the file 'iso3166.tab'. +# 2. Latitude and longitude of the timezone's principal location +# in ISO 6709 sign-degrees-minutes-seconds format, +# either ±DDMM±DDDMM or ±DDMMSS±DDDMMSS, +# first latitude (+ is north), then longitude (+ is east). +# 3. Timezone name used in value of TZ environment variable. +# Please see the theory.html file for how these names are chosen. +# If multiple timezones overlap a country, each has a row in the +# table, with each column 1 containing the country code. +# 4. Comments; present if and only if a country has multiple timezones. +# +# If a timezone covers multiple countries, the most-populous city is used, +# and that country is listed first in column 1; any other countries +# are listed alphabetically by country code. The table is sorted +# first by country code, then (if possible) by an order within the +# country that (1) makes some geographical sense, and (2) puts the +# most populous timezones first, where that does not contradict (1). +# +# This table is intended as an aid for users, to help them select timezones +# appropriate for their practical needs. It is not intended to take or +# endorse any position on legal or territorial claims. +# +#country- +#codes coordinates TZ comments +AD +4230+00131 Europe/Andorra +AE,OM +2518+05518 Asia/Dubai +AF +3431+06912 Asia/Kabul +AL +4120+01950 Europe/Tirane +AM +4011+04430 Asia/Yerevan +AQ -6617+11031 Antarctica/Casey Casey +AQ -6835+07758 Antarctica/Davis Davis +AQ -6640+14001 Antarctica/DumontDUrville Dumont-d'Urville +AQ -6736+06253 Antarctica/Mawson Mawson +AQ -6448-06406 Antarctica/Palmer Palmer +AQ -6734-06808 Antarctica/Rothera Rothera +AQ -690022+0393524 Antarctica/Syowa Syowa +AQ -720041+0023206 Antarctica/Troll Troll +AQ -7824+10654 Antarctica/Vostok Vostok +AR -3436-05827 America/Argentina/Buenos_Aires Buenos Aires (BA, CF) +AR -3124-06411 America/Argentina/Cordoba Argentina (most areas: CB, CC, CN, ER, FM, MN, SE, SF) +AR -2447-06525 America/Argentina/Salta Salta (SA, LP, NQ, RN) +AR -2411-06518 America/Argentina/Jujuy Jujuy (JY) +AR -2649-06513 America/Argentina/Tucuman Tucumán (TM) +AR -2828-06547 America/Argentina/Catamarca Catamarca (CT); Chubut (CH) +AR -2926-06651 America/Argentina/La_Rioja La Rioja (LR) +AR -3132-06831 America/Argentina/San_Juan San Juan (SJ) +AR -3253-06849 America/Argentina/Mendoza Mendoza (MZ) +AR -3319-06621 America/Argentina/San_Luis San Luis (SL) +AR -5138-06913 America/Argentina/Rio_Gallegos Santa Cruz (SC) +AR -5448-06818 America/Argentina/Ushuaia Tierra del Fuego (TF) +AS,UM -1416-17042 Pacific/Pago_Pago Samoa, Midway +AT +4813+01620 Europe/Vienna +AU -3133+15905 Australia/Lord_Howe Lord Howe Island +AU -5430+15857 Antarctica/Macquarie Macquarie Island +AU -4253+14719 Australia/Hobart Tasmania (most areas) +AU -3956+14352 Australia/Currie Tasmania (King Island) +AU -3749+14458 Australia/Melbourne Victoria +AU -3352+15113 Australia/Sydney New South Wales (most areas) +AU -3157+14127 Australia/Broken_Hill New South Wales (Yancowinna) +AU -2728+15302 Australia/Brisbane Queensland (most areas) +AU -2016+14900 Australia/Lindeman Queensland (Whitsunday Islands) +AU -3455+13835 Australia/Adelaide South Australia +AU -1228+13050 Australia/Darwin Northern Territory +AU -3157+11551 Australia/Perth Western Australia (most areas) +AU -3143+12852 Australia/Eucla Western Australia (Eucla) +AZ +4023+04951 Asia/Baku +BB +1306-05937 America/Barbados +BD +2343+09025 Asia/Dhaka +BE +5050+00420 Europe/Brussels +BG +4241+02319 Europe/Sofia +BM +3217-06446 Atlantic/Bermuda +BN +0456+11455 Asia/Brunei +BO -1630-06809 America/La_Paz +BR -0351-03225 America/Noronha Atlantic islands +BR -0127-04829 America/Belem Pará (east); Amapá +BR -0343-03830 America/Fortaleza Brazil (northeast: MA, PI, CE, RN, PB) +BR -0803-03454 America/Recife Pernambuco +BR -0712-04812 America/Araguaina Tocantins +BR -0940-03543 America/Maceio Alagoas, Sergipe +BR -1259-03831 America/Bahia Bahia +BR -2332-04637 America/Sao_Paulo Brazil (southeast: GO, DF, MG, ES, RJ, SP, PR, SC, RS) +BR -2027-05437 America/Campo_Grande Mato Grosso do Sul +BR -1535-05605 America/Cuiaba Mato Grosso +BR -0226-05452 America/Santarem Pará (west) +BR -0846-06354 America/Porto_Velho Rondônia +BR +0249-06040 America/Boa_Vista Roraima +BR -0308-06001 America/Manaus Amazonas (east) +BR -0640-06952 America/Eirunepe Amazonas (west) +BR -0958-06748 America/Rio_Branco Acre +BS +2505-07721 America/Nassau +BT +2728+08939 Asia/Thimphu +BY +5354+02734 Europe/Minsk +BZ +1730-08812 America/Belize +CA +4734-05243 America/St_Johns Newfoundland; Labrador (southeast) +CA +4439-06336 America/Halifax Atlantic - NS (most areas); PE +CA +4612-05957 America/Glace_Bay Atlantic - NS (Cape Breton) +CA +4606-06447 America/Moncton Atlantic - New Brunswick +CA +5320-06025 America/Goose_Bay Atlantic - Labrador (most areas) +CA +5125-05707 America/Blanc-Sablon AST - QC (Lower North Shore) +CA +4339-07923 America/Toronto Eastern - ON, QC (most areas) +CA +4901-08816 America/Nipigon Eastern - ON, QC (no DST 1967-73) +CA +4823-08915 America/Thunder_Bay Eastern - ON (Thunder Bay) +CA +6344-06828 America/Iqaluit Eastern - NU (most east areas) +CA +6608-06544 America/Pangnirtung Eastern - NU (Pangnirtung) +CA +484531-0913718 America/Atikokan EST - ON (Atikokan); NU (Coral H) +CA +4953-09709 America/Winnipeg Central - ON (west); Manitoba +CA +4843-09434 America/Rainy_River Central - ON (Rainy R, Ft Frances) +CA +744144-0944945 America/Resolute Central - NU (Resolute) +CA +624900-0920459 America/Rankin_Inlet Central - NU (central) +CA +5024-10439 America/Regina CST - SK (most areas) +CA +5017-10750 America/Swift_Current CST - SK (midwest) +CA +5333-11328 America/Edmonton Mountain - AB; BC (E); SK (W) +CA +690650-1050310 America/Cambridge_Bay Mountain - NU (west) +CA +6227-11421 America/Yellowknife Mountain - NT (central) +CA +682059-1334300 America/Inuvik Mountain - NT (west) +CA +4906-11631 America/Creston MST - BC (Creston) +CA +5946-12014 America/Dawson_Creek MST - BC (Dawson Cr, Ft St John) +CA +5848-12242 America/Fort_Nelson MST - BC (Ft Nelson) +CA +4916-12307 America/Vancouver Pacific - BC (most areas) +CA +6043-13503 America/Whitehorse Pacific - Yukon (south) +CA +6404-13925 America/Dawson Pacific - Yukon (north) +CC -1210+09655 Indian/Cocos +CH,DE,LI +4723+00832 Europe/Zurich Swiss time +CI,BF,GM,GN,ML,MR,SH,SL,SN,TG +0519-00402 Africa/Abidjan +CK -2114-15946 Pacific/Rarotonga +CL -3327-07040 America/Santiago Chile (most areas) +CL -5309-07055 America/Punta_Arenas Region of Magallanes +CL -2709-10926 Pacific/Easter Easter Island +CN +3114+12128 Asia/Shanghai Beijing Time +CN +4348+08735 Asia/Urumqi Xinjiang Time +CO +0436-07405 America/Bogota +CR +0956-08405 America/Costa_Rica +CU +2308-08222 America/Havana +CV +1455-02331 Atlantic/Cape_Verde +CW,AW,BQ,SX +1211-06900 America/Curacao +CX -1025+10543 Indian/Christmas +CY +3510+03322 Asia/Nicosia Cyprus (most areas) +CY +3507+03357 Asia/Famagusta Northern Cyprus +CZ,SK +5005+01426 Europe/Prague +DE +5230+01322 Europe/Berlin Germany (most areas) +DK +5540+01235 Europe/Copenhagen +DO +1828-06954 America/Santo_Domingo +DZ +3647+00303 Africa/Algiers +EC -0210-07950 America/Guayaquil Ecuador (mainland) +EC -0054-08936 Pacific/Galapagos Galápagos Islands +EE +5925+02445 Europe/Tallinn +EG +3003+03115 Africa/Cairo +EH +2709-01312 Africa/El_Aaiun +ES +4024-00341 Europe/Madrid Spain (mainland) +ES +3553-00519 Africa/Ceuta Ceuta, Melilla +ES +2806-01524 Atlantic/Canary Canary Islands +FI,AX +6010+02458 Europe/Helsinki +FJ -1808+17825 Pacific/Fiji +FK -5142-05751 Atlantic/Stanley +FM +0725+15147 Pacific/Chuuk Chuuk/Truk, Yap +FM +0658+15813 Pacific/Pohnpei Pohnpei/Ponape +FM +0519+16259 Pacific/Kosrae Kosrae +FO +6201-00646 Atlantic/Faroe +FR +4852+00220 Europe/Paris +GB,GG,IM,JE +513030-0000731 Europe/London +GE +4143+04449 Asia/Tbilisi +GF +0456-05220 America/Cayenne +GH +0533-00013 Africa/Accra +GI +3608-00521 Europe/Gibraltar +GL +6411-05144 America/Godthab Greenland (most areas) +GL +7646-01840 America/Danmarkshavn National Park (east coast) +GL +7029-02158 America/Scoresbysund Scoresbysund/Ittoqqortoormiit +GL +7634-06847 America/Thule Thule/Pituffik +GR +3758+02343 Europe/Athens +GS -5416-03632 Atlantic/South_Georgia +GT +1438-09031 America/Guatemala +GU,MP +1328+14445 Pacific/Guam +GW +1151-01535 Africa/Bissau +GY +0648-05810 America/Guyana +HK +2217+11409 Asia/Hong_Kong +HN +1406-08713 America/Tegucigalpa +HT +1832-07220 America/Port-au-Prince +HU +4730+01905 Europe/Budapest +ID -0610+10648 Asia/Jakarta Java, Sumatra +ID -0002+10920 Asia/Pontianak Borneo (west, central) +ID -0507+11924 Asia/Makassar Borneo (east, south); Sulawesi/Celebes, Bali, Nusa Tengarra; Timor (west) +ID -0232+14042 Asia/Jayapura New Guinea (West Papua / Irian Jaya); Malukus/Moluccas +IE +5320-00615 Europe/Dublin +IL +314650+0351326 Asia/Jerusalem +IN +2232+08822 Asia/Kolkata +IO -0720+07225 Indian/Chagos +IQ +3321+04425 Asia/Baghdad +IR +3540+05126 Asia/Tehran +IS +6409-02151 Atlantic/Reykjavik +IT,SM,VA +4154+01229 Europe/Rome +JM +175805-0764736 America/Jamaica +JO +3157+03556 Asia/Amman +JP +353916+1394441 Asia/Tokyo +KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT -0117+03649 Africa/Nairobi +KG +4254+07436 Asia/Bishkek +KI +0125+17300 Pacific/Tarawa Gilbert Islands +KI -0308-17105 Pacific/Enderbury Phoenix Islands +KI +0152-15720 Pacific/Kiritimati Line Islands +KP +3901+12545 Asia/Pyongyang +KR +3733+12658 Asia/Seoul +KZ +4315+07657 Asia/Almaty Kazakhstan (most areas) +KZ +4448+06528 Asia/Qyzylorda Qyzylorda/Kyzylorda/Kzyl-Orda +KZ +5017+05710 Asia/Aqtobe Aqtöbe/Aktobe +KZ +4431+05016 Asia/Aqtau Mangghystaū/Mankistau +KZ +4707+05156 Asia/Atyrau Atyraū/Atirau/Gur'yev +KZ +5113+05121 Asia/Oral West Kazakhstan +LB +3353+03530 Asia/Beirut +LK +0656+07951 Asia/Colombo +LR +0618-01047 Africa/Monrovia +LT +5441+02519 Europe/Vilnius +LU +4936+00609 Europe/Luxembourg +LV +5657+02406 Europe/Riga +LY +3254+01311 Africa/Tripoli +MA +3339-00735 Africa/Casablanca +MC +4342+00723 Europe/Monaco +MD +4700+02850 Europe/Chisinau +MH +0709+17112 Pacific/Majuro Marshall Islands (most areas) +MH +0905+16720 Pacific/Kwajalein Kwajalein +MM +1647+09610 Asia/Yangon +MN +4755+10653 Asia/Ulaanbaatar Mongolia (most areas) +MN +4801+09139 Asia/Hovd Bayan-Ölgii, Govi-Altai, Hovd, Uvs, Zavkhan +MN +4804+11430 Asia/Choibalsan Dornod, Sükhbaatar +MO +221150+1133230 Asia/Macau +MQ +1436-06105 America/Martinique +MT +3554+01431 Europe/Malta +MU -2010+05730 Indian/Mauritius +MV +0410+07330 Indian/Maldives +MX +1924-09909 America/Mexico_City Central Time +MX +2105-08646 America/Cancun Eastern Standard Time - Quintana Roo +MX +2058-08937 America/Merida Central Time - Campeche, Yucatán +MX +2540-10019 America/Monterrey Central Time - Durango; Coahuila, Nuevo León, Tamaulipas (most areas) +MX +2550-09730 America/Matamoros Central Time US - Coahuila, Nuevo León, Tamaulipas (US border) +MX +2313-10625 America/Mazatlan Mountain Time - Baja California Sur, Nayarit, Sinaloa +MX +2838-10605 America/Chihuahua Mountain Time - Chihuahua (most areas) +MX +2934-10425 America/Ojinaga Mountain Time US - Chihuahua (US border) +MX +2904-11058 America/Hermosillo Mountain Standard Time - Sonora +MX +3232-11701 America/Tijuana Pacific Time US - Baja California +MX +2048-10515 America/Bahia_Banderas Central Time - Bahía de Banderas +MY +0310+10142 Asia/Kuala_Lumpur Malaysia (peninsula) +MY +0133+11020 Asia/Kuching Sabah, Sarawak +MZ,BI,BW,CD,MW,RW,ZM,ZW -2558+03235 Africa/Maputo Central Africa Time +NA -2234+01706 Africa/Windhoek +NC -2216+16627 Pacific/Noumea +NF -2903+16758 Pacific/Norfolk +NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE +0627+00324 Africa/Lagos West Africa Time +NI +1209-08617 America/Managua +NL +5222+00454 Europe/Amsterdam +NO,SJ +5955+01045 Europe/Oslo +NP +2743+08519 Asia/Kathmandu +NR -0031+16655 Pacific/Nauru +NU -1901-16955 Pacific/Niue +NZ,AQ -3652+17446 Pacific/Auckland New Zealand time +NZ -4357-17633 Pacific/Chatham Chatham Islands +PA,KY +0858-07932 America/Panama +PE -1203-07703 America/Lima +PF -1732-14934 Pacific/Tahiti Society Islands +PF -0900-13930 Pacific/Marquesas Marquesas Islands +PF -2308-13457 Pacific/Gambier Gambier Islands +PG -0930+14710 Pacific/Port_Moresby Papua New Guinea (most areas) +PG -0613+15534 Pacific/Bougainville Bougainville +PH +1435+12100 Asia/Manila +PK +2452+06703 Asia/Karachi +PL +5215+02100 Europe/Warsaw +PM +4703-05620 America/Miquelon +PN -2504-13005 Pacific/Pitcairn +PR +182806-0660622 America/Puerto_Rico +PS +3130+03428 Asia/Gaza Gaza Strip +PS +313200+0350542 Asia/Hebron West Bank +PT +3843-00908 Europe/Lisbon Portugal (mainland) +PT +3238-01654 Atlantic/Madeira Madeira Islands +PT +3744-02540 Atlantic/Azores Azores +PW +0720+13429 Pacific/Palau +PY -2516-05740 America/Asuncion +QA,BH +2517+05132 Asia/Qatar +RE,TF -2052+05528 Indian/Reunion Réunion, Crozet, Scattered Islands +RO +4426+02606 Europe/Bucharest +RS,BA,HR,ME,MK,SI +4450+02030 Europe/Belgrade +RU +5443+02030 Europe/Kaliningrad MSK-01 - Kaliningrad +RU +554521+0373704 Europe/Moscow MSK+00 - Moscow area +RU +4457+03406 Europe/Simferopol MSK+00 - Crimea +RU +4844+04425 Europe/Volgograd MSK+00 - Volgograd +RU +5836+04939 Europe/Kirov MSK+00 - Kirov +RU +4621+04803 Europe/Astrakhan MSK+01 - Astrakhan +RU +5134+04602 Europe/Saratov MSK+01 - Saratov +RU +5420+04824 Europe/Ulyanovsk MSK+01 - Ulyanovsk +RU +5312+05009 Europe/Samara MSK+01 - Samara, Udmurtia +RU +5651+06036 Asia/Yekaterinburg MSK+02 - Urals +RU +5500+07324 Asia/Omsk MSK+03 - Omsk +RU +5502+08255 Asia/Novosibirsk MSK+04 - Novosibirsk +RU +5322+08345 Asia/Barnaul MSK+04 - Altai +RU +5630+08458 Asia/Tomsk MSK+04 - Tomsk +RU +5345+08707 Asia/Novokuznetsk MSK+04 - Kemerovo +RU +5601+09250 Asia/Krasnoyarsk MSK+04 - Krasnoyarsk area +RU +5216+10420 Asia/Irkutsk MSK+05 - Irkutsk, Buryatia +RU +5203+11328 Asia/Chita MSK+06 - Zabaykalsky +RU +6200+12940 Asia/Yakutsk MSK+06 - Lena River +RU +623923+1353314 Asia/Khandyga MSK+06 - Tomponsky, Ust-Maysky +RU +4310+13156 Asia/Vladivostok MSK+07 - Amur River +RU +643337+1431336 Asia/Ust-Nera MSK+07 - Oymyakonsky +RU +5934+15048 Asia/Magadan MSK+08 - Magadan +RU +4658+14242 Asia/Sakhalin MSK+08 - Sakhalin Island +RU +6728+15343 Asia/Srednekolymsk MSK+08 - Sakha (E); North Kuril Is +RU +5301+15839 Asia/Kamchatka MSK+09 - Kamchatka +RU +6445+17729 Asia/Anadyr MSK+09 - Bering Sea +SA,KW,YE +2438+04643 Asia/Riyadh +SB -0932+16012 Pacific/Guadalcanal +SC -0440+05528 Indian/Mahe +SD +1536+03232 Africa/Khartoum +SE +5920+01803 Europe/Stockholm +SG +0117+10351 Asia/Singapore +SR +0550-05510 America/Paramaribo +SS +0451+03137 Africa/Juba +ST +0020+00644 Africa/Sao_Tome +SV +1342-08912 America/El_Salvador +SY +3330+03618 Asia/Damascus +TC +2128-07108 America/Grand_Turk +TD +1207+01503 Africa/Ndjamena +TF -492110+0701303 Indian/Kerguelen Kerguelen, St Paul Island, Amsterdam Island +TH,KH,LA,VN +1345+10031 Asia/Bangkok Indochina (most areas) +TJ +3835+06848 Asia/Dushanbe +TK -0922-17114 Pacific/Fakaofo +TL -0833+12535 Asia/Dili +TM +3757+05823 Asia/Ashgabat +TN +3648+01011 Africa/Tunis +TO -2110-17510 Pacific/Tongatapu +TR +4101+02858 Europe/Istanbul +TT,AG,AI,BL,DM,GD,GP,KN,LC,MF,MS,VC,VG,VI +1039-06131 America/Port_of_Spain +TV -0831+17913 Pacific/Funafuti +TW +2503+12130 Asia/Taipei +UA +5026+03031 Europe/Kiev Ukraine (most areas) +UA +4837+02218 Europe/Uzhgorod Ruthenia +UA +4750+03510 Europe/Zaporozhye Zaporozh'ye/Zaporizhia; Lugansk/Luhansk (east) +UM +1917+16637 Pacific/Wake Wake Island +US +404251-0740023 America/New_York Eastern (most areas) +US +421953-0830245 America/Detroit Eastern - MI (most areas) +US +381515-0854534 America/Kentucky/Louisville Eastern - KY (Louisville area) +US +364947-0845057 America/Kentucky/Monticello Eastern - KY (Wayne) +US +394606-0860929 America/Indiana/Indianapolis Eastern - IN (most areas) +US +384038-0873143 America/Indiana/Vincennes Eastern - IN (Da, Du, K, Mn) +US +410305-0863611 America/Indiana/Winamac Eastern - IN (Pulaski) +US +382232-0862041 America/Indiana/Marengo Eastern - IN (Crawford) +US +382931-0871643 America/Indiana/Petersburg Eastern - IN (Pike) +US +384452-0850402 America/Indiana/Vevay Eastern - IN (Switzerland) +US +415100-0873900 America/Chicago Central (most areas) +US +375711-0864541 America/Indiana/Tell_City Central - IN (Perry) +US +411745-0863730 America/Indiana/Knox Central - IN (Starke) +US +450628-0873651 America/Menominee Central - MI (Wisconsin border) +US +470659-1011757 America/North_Dakota/Center Central - ND (Oliver) +US +465042-1012439 America/North_Dakota/New_Salem Central - ND (Morton rural) +US +471551-1014640 America/North_Dakota/Beulah Central - ND (Mercer) +US +394421-1045903 America/Denver Mountain (most areas) +US +433649-1161209 America/Boise Mountain - ID (south); OR (east) +US +332654-1120424 America/Phoenix MST - Arizona (except Navajo) +US +340308-1181434 America/Los_Angeles Pacific +US +611305-1495401 America/Anchorage Alaska (most areas) +US +581807-1342511 America/Juneau Alaska - Juneau area +US +571035-1351807 America/Sitka Alaska - Sitka area +US +550737-1313435 America/Metlakatla Alaska - Annette Island +US +593249-1394338 America/Yakutat Alaska - Yakutat +US +643004-1652423 America/Nome Alaska (west) +US +515248-1763929 America/Adak Aleutian Islands +US,UM +211825-1575130 Pacific/Honolulu Hawaii +UY -345433-0561245 America/Montevideo +UZ +3940+06648 Asia/Samarkand Uzbekistan (west) +UZ +4120+06918 Asia/Tashkent Uzbekistan (east) +VE +1030-06656 America/Caracas +VN +1045+10640 Asia/Ho_Chi_Minh Vietnam (south) +VU -1740+16825 Pacific/Efate +WF -1318-17610 Pacific/Wallis +WS -1350-17144 Pacific/Apia +ZA,LS,SZ -2615+02800 Africa/Johannesburg diff --git a/libs/rarfile.py b/libs/rarfile.py index 25b61196..78148c19 100644 --- a/libs/rarfile.py +++ b/libs/rarfile.py @@ -54,74 +54,127 @@ here they are with defaults, and reason to change it:: # Set to full path of unrar.exe if it is not in PATH rarfile.UNRAR_TOOL = "unrar" - # Set to 0 if you don't look at comments and want to - # avoid wasting time for parsing them - rarfile.NEED_COMMENTS = 1 - - # Set up to 1 if you don't want to deal with decoding comments - # from unknown encoding. rarfile will try couple of common - # encodings in sequence. - rarfile.UNICODE_COMMENTS = 0 - - # Set to 1 if you prefer timestamps to be datetime objects - # instead tuples - rarfile.USE_DATETIME = 0 - - # Set to '/' to be more compatible with zipfile - rarfile.PATH_SEP = '\\' + # Set to '\\' to be more compatible with old rarfile + rarfile.PATH_SEP = '/' For more details, refer to source. """ -__version__ = '2.8' - -# export only interesting items -__all__ = ['is_rarfile', 'RarInfo', 'RarFile', 'RarExtFile'] +from __future__ import division, print_function ## ## Imports and compat - support both Python 2.x and 3.x ## -import sys, os, struct, errno +import sys +import os +import errno +import struct + from struct import pack, unpack, Struct -from binascii import crc32 +from binascii import crc32, hexlify from tempfile import mkstemp from subprocess import Popen, PIPE, STDOUT -from datetime import datetime from io import RawIOBase -from hashlib import sha1 +from hashlib import sha1, sha256 +from hmac import HMAC +from datetime import datetime, timedelta, tzinfo + +# fixed offset timezone, for UTC +try: + from datetime import timezone +except ImportError: + class timezone(tzinfo): + """Compat timezone.""" + __slots__ = ('_ofs', '_name') + _DST = timedelta(0) + + def __init__(self, offset, name): + super(timezone, self).__init__() + self._ofs, self._name = offset, name + + def utcoffset(self, dt): + return self._ofs + + def tzname(self, dt): + return self._name + + def dst(self, dt): + return self._DST # only needed for encryped headers try: try: from cryptography.hazmat.primitives.ciphers import algorithms, modes, Cipher from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.kdf import pbkdf2 + class AES_CBC_Decrypt(object): - block_size = 16 + """Decrypt API""" def __init__(self, key, iv): ciph = Cipher(algorithms.AES(key), modes.CBC(iv), default_backend()) - self.dec = ciph.decryptor() - def decrypt(self, data): - return self.dec.update(data) + self.decrypt = ciph.decryptor().update + + def pbkdf2_sha256(password, salt, iters): + """PBKDF2 with HMAC-SHA256""" + ctx = pbkdf2.PBKDF2HMAC(hashes.SHA256(), 32, salt, iters, default_backend()) + return ctx.derive(password) + except ImportError: from Crypto.Cipher import AES + from Crypto.Protocol import KDF + class AES_CBC_Decrypt(object): - block_size = 16 + """Decrypt API""" def __init__(self, key, iv): - self.dec = AES.new(key, AES.MODE_CBC, iv) - def decrypt(self, data): - return self.dec.decrypt(data) + self.decrypt = AES.new(key, AES.MODE_CBC, iv).decrypt + + def pbkdf2_sha256(password, salt, iters): + """PBKDF2 with HMAC-SHA256""" + return KDF.PBKDF2(password, salt, 32, iters, hmac_sha256) + _have_crypto = 1 except ImportError: _have_crypto = 0 +try: + try: + from hashlib import blake2s + _have_blake2 = True + except ImportError: + from pyblake2 import blake2s + _have_blake2 = True +except ImportError: + _have_blake2 = False + # compat with 2.x if sys.hexversion < 0x3000000: - # prefer 3.x behaviour - range = xrange -else: + def rar_crc32(data, prev=0): + """CRC32 with unsigned values. + """ + if (prev > 0) and (prev & 0x80000000): + prev -= (1 << 32) + res = crc32(data, prev) + if res < 0: + res += (1 << 32) + return res + tohex = hexlify + _byte_code = ord +else: # pragma: no cover + def tohex(data): + """Return hex string.""" + return hexlify(data).decode('ascii') + rar_crc32 = crc32 unicode = str + _byte_code = int # noqa + + +__version__ = '3.0' + +# export only interesting items +__all__ = ['is_rarfile', 'RarInfo', 'RarFile', 'RarExtFile'] ## ## Module configuration. Can be tuned after importing. @@ -166,36 +219,27 @@ ALT_CHECK_ARGS = ('--help',) USE_EXTRACT_HACK = 1 #: limit the filesize for tmp archive usage -HACK_SIZE_LIMIT = 20*1024*1024 - -#: whether to parse file/archive comments. -NEED_COMMENTS = 1 - -#: whether to convert comments to unicode strings -UNICODE_COMMENTS = 0 - -#: Convert RAR time tuple into datetime() object -USE_DATETIME = 0 +HACK_SIZE_LIMIT = 20 * 1024 * 1024 #: Separator for path name components. RAR internally uses '\\'. #: Use '/' to be similar with zipfile. -PATH_SEP = '\\' +PATH_SEP = '/' ## ## rar constants ## # block types -RAR_BLOCK_MARK = 0x72 # r -RAR_BLOCK_MAIN = 0x73 # s -RAR_BLOCK_FILE = 0x74 # t -RAR_BLOCK_OLD_COMMENT = 0x75 # u -RAR_BLOCK_OLD_EXTRA = 0x76 # v -RAR_BLOCK_OLD_SUB = 0x77 # w -RAR_BLOCK_OLD_RECOVERY = 0x78 # x -RAR_BLOCK_OLD_AUTH = 0x79 # y -RAR_BLOCK_SUB = 0x7a # z -RAR_BLOCK_ENDARC = 0x7b # { +RAR_BLOCK_MARK = 0x72 # r +RAR_BLOCK_MAIN = 0x73 # s +RAR_BLOCK_FILE = 0x74 # t +RAR_BLOCK_OLD_COMMENT = 0x75 # u +RAR_BLOCK_OLD_EXTRA = 0x76 # v +RAR_BLOCK_OLD_SUB = 0x77 # w +RAR_BLOCK_OLD_RECOVERY = 0x78 # x +RAR_BLOCK_OLD_AUTH = 0x79 # y +RAR_BLOCK_SUB = 0x7a # z +RAR_BLOCK_ENDARC = 0x7b # { # flags for RAR_BLOCK_MAIN RAR_MAIN_VOLUME = 0x0001 @@ -257,196 +301,335 @@ RAR_M3 = 0x33 RAR_M4 = 0x34 RAR_M5 = 0x35 +# +# RAR5 constants +# + +RAR5_BLOCK_MAIN = 1 +RAR5_BLOCK_FILE = 2 +RAR5_BLOCK_SERVICE = 3 +RAR5_BLOCK_ENCRYPTION = 4 +RAR5_BLOCK_ENDARC = 5 + +RAR5_BLOCK_FLAG_EXTRA_DATA = 0x01 +RAR5_BLOCK_FLAG_DATA_AREA = 0x02 +RAR5_BLOCK_FLAG_SKIP_IF_UNKNOWN = 0x04 +RAR5_BLOCK_FLAG_SPLIT_BEFORE = 0x08 +RAR5_BLOCK_FLAG_SPLIT_AFTER = 0x10 +RAR5_BLOCK_FLAG_DEPENDS_PREV = 0x20 +RAR5_BLOCK_FLAG_KEEP_WITH_PARENT = 0x40 + +RAR5_MAIN_FLAG_ISVOL = 0x01 +RAR5_MAIN_FLAG_HAS_VOLNR = 0x02 +RAR5_MAIN_FLAG_SOLID = 0x04 +RAR5_MAIN_FLAG_RECOVERY = 0x08 +RAR5_MAIN_FLAG_LOCKED = 0x10 + +RAR5_FILE_FLAG_ISDIR = 0x01 +RAR5_FILE_FLAG_HAS_MTIME = 0x02 +RAR5_FILE_FLAG_HAS_CRC32 = 0x04 +RAR5_FILE_FLAG_UNKNOWN_SIZE = 0x08 + +RAR5_COMPR_SOLID = 0x40 + +RAR5_ENC_FLAG_HAS_CHECKVAL = 0x01 + +RAR5_ENDARC_FLAG_NEXT_VOL = 0x01 + +RAR5_XFILE_ENCRYPTION = 1 +RAR5_XFILE_HASH = 2 +RAR5_XFILE_TIME = 3 +RAR5_XFILE_VERSION = 4 +RAR5_XFILE_REDIR = 5 +RAR5_XFILE_OWNER = 6 +RAR5_XFILE_SERVICE = 7 + +RAR5_XTIME_UNIXTIME = 0x01 +RAR5_XTIME_HAS_MTIME = 0x02 +RAR5_XTIME_HAS_CTIME = 0x04 +RAR5_XTIME_HAS_ATIME = 0x08 + +RAR5_XENC_CIPHER_AES256 = 0 + +RAR5_XENC_CHECKVAL = 0x01 +RAR5_XENC_TWEAKED = 0x02 + +RAR5_XHASH_BLAKE2SP = 0 + +RAR5_XREDIR_UNIX_SYMLINK = 1 +RAR5_XREDIR_WINDOWS_SYMLINK = 2 +RAR5_XREDIR_WINDOWS_JUNCTION = 3 +RAR5_XREDIR_HARD_LINK = 4 +RAR5_XREDIR_FILE_COPY = 5 + +RAR5_XREDIR_ISDIR = 0x01 + +RAR5_XOWNER_UNAME = 0x01 +RAR5_XOWNER_GNAME = 0x02 +RAR5_XOWNER_UID = 0x04 +RAR5_XOWNER_GID = 0x08 + +RAR5_OS_WINDOWS = 0 +RAR5_OS_UNIX = 1 + ## ## internal constants ## RAR_ID = b"Rar!\x1a\x07\x00" -ZERO = b"\0" -EMPTY = b"" +RAR5_ID = b"Rar!\x1a\x07\x01\x00" +ZERO = b'\0' +EMPTY = b'' +UTC = timezone(timedelta(0), 'UTC') +BSIZE = 32 * 1024 -S_BLK_HDR = Struct(' 0 + class Error(Exception): """Base class for rarfile errors.""" + class BadRarFile(Error): """Incorrect data in archive.""" + class NotRarFile(Error): """The file is not RAR archive.""" + class BadRarName(Error): """Cannot guess multipart name components.""" + class NoRarEntry(Error): """File not found in RAR""" + class PasswordRequired(Error): """File requires password""" + class NeedFirstVolume(Error): """Need to start from first volume.""" + class NoCrypto(Error): """Cannot parse encrypted headers - no crypto available.""" + class RarExecError(Error): """Problem reported by unrar/rar.""" + class RarWarning(RarExecError): """Non-fatal error""" + class RarFatalError(RarExecError): """Fatal error""" + class RarCRCError(RarExecError): """CRC error during unpacking""" + class RarLockedArchiveError(RarExecError): """Must not modify locked archive""" + class RarWriteError(RarExecError): """Write error""" + class RarOpenError(RarExecError): """Open error""" + class RarUserError(RarExecError): """User error""" + class RarMemoryError(RarExecError): """Memory error""" + class RarCreateError(RarExecError): """Create error""" + class RarNoFilesError(RarExecError): """No files that match pattern were found""" + class RarUserBreak(RarExecError): """User stop""" + +class RarWrongPassword(RarExecError): + """Incorrect password""" + class RarUnknownError(RarExecError): """Unknown exit code""" + class RarSignalExit(RarExecError): """Unrar exited with signal""" + class RarCannotExec(RarExecError): """Executable not found.""" -def is_rarfile(xfile): - '''Check quickly whether file is rar archive.''' - fd = XFile(xfile) - buf = fd.read(len(RAR_ID)) - fd.close() - return buf == RAR_ID - - class RarInfo(object): - r'''An entry in rar archive. + r"""An entry in rar archive. + + RAR3 extended timestamps are :class:`datetime.datetime` objects without timezone. + RAR5 extended timestamps are :class:`datetime.datetime` objects with UTC timezone. + + Attributes: - :mod:`zipfile`-compatible fields: - filename File name with relative path. - Default path separator is '\\', to change set rarfile.PATH_SEP. - Always unicode string. + Path separator is '/'. Always unicode string. + date_time - Modification time, tuple of (year, month, day, hour, minute, second). - Or datetime() object if USE_DATETIME is set. + File modification timestamp. As tuple of (year, month, day, hour, minute, second). + RAR5 allows archives where it is missing, it's None then. + file_size Uncompressed size. + compress_size Compressed size. + + compress_type + Compression method: one of :data:`RAR_M0` .. :data:`RAR_M5` constants. + + extract_version + Minimal Rar version needed for decompressing. As (major*10 + minor), + so 2.9 is 29. + + RAR3: 10, 20, 29 + + RAR5 does not have such field in archive, it's simply set to 50. + + host_os + Host OS type, one of RAR_OS_* constants. + + RAR3: :data:`RAR_OS_WIN32`, :data:`RAR_OS_UNIX`, :data:`RAR_OS_MSDOS`, + :data:`RAR_OS_OS2`, :data:`RAR_OS_BEOS`. + + RAR5: :data:`RAR_OS_WIN32`, :data:`RAR_OS_UNIX`. + + mode + File attributes. May be either dos-style or unix-style, depending on host_os. + + mtime + File modification time. Same value as :attr:`date_time` + but as :class:`datetime.datetime` object with extended precision. + + ctime + Optional time field: creation time. As :class:`datetime.datetime` object. + + atime + Optional time field: last access time. As :class:`datetime.datetime` object. + + arctime + Optional time field: archival time. As :class:`datetime.datetime` object. + (RAR3-only) + CRC CRC-32 of uncompressed file, unsigned int. + + RAR5: may be None. + + blake2sp_hash + Blake2SP hash over decompressed data. (RAR5-only) + comment - File comment. Byte string or None. Use UNICODE_COMMENTS - to get automatic decoding to unicode. + Optional file comment field. Unicode string. (RAR3-only) + + file_redir + If not None, file is link of some sort. Contains tuple of (type, flags, target). + (RAR5-only) + + Type is one of constants: + + :data:`RAR5_XREDIR_UNIX_SYMLINK` + unix symlink to target. + :data:`RAR5_XREDIR_WINDOWS_SYMLINK` + windows symlink to target. + :data:`RAR5_XREDIR_WINDOWS_JUNCTION` + windows junction. + :data:`RAR5_XREDIR_HARD_LINK` + hard link to target. + :data:`RAR5_XREDIR_FILE_COPY` + current file is copy of another archive entry. + + Flags may contain :data:`RAR5_XREDIR_ISDIR` bit. + volume Volume nr, starting from 0. - RAR-specific fields: - - compress_type - Compression method: 0x30 - 0x35. - extract_version - Minimal Rar version needed for decompressing. - host_os - Host OS type, one of RAR_OS_* constants. - mode - File attributes. May be either dos-style or unix-style, depending on host_os. volume_file Volume file name, where file starts. - mtime - Optional time field: Modification time, with float seconds. - Same as .date_time but with more precision. - ctime - Optional time field: creation time, with float seconds. - atime - Optional time field: last access time, with float seconds. - arctime - Optional time field: archival time, with float seconds. - Internal fields: + """ - type - One of RAR_BLOCK_* types. Only entries with type==RAR_BLOCK_FILE are shown in .infolist(). - flags - For files, RAR_FILE_* bits. - ''' + # zipfile-compatible fields + filename = None + file_size = None + compress_size = None + date_time = None + comment = None + CRC = None + volume = None + orig_filename = None - __slots__ = ( - # zipfile-compatible fields - 'filename', - 'file_size', - 'compress_size', - 'date_time', - 'comment', - 'CRC', - 'volume', - 'orig_filename', # bytes in unknown encoding + # optional extended time fields, datetime() objects. + mtime = None + ctime = None + atime = None - # rar-specific fields - 'extract_version', - 'compress_type', - 'host_os', - 'mode', - 'type', - 'flags', + extract_version = None + mode = None + host_os = None + compress_type = None - # optional extended time fields - # tuple where the sec is float, or datetime(). - 'mtime', # same as .date_time - 'ctime', - 'atime', - 'arctime', + # rar3-only fields + comment = None + arctime = None - # RAR internals - 'name_size', - 'header_size', - 'header_crc', - 'file_offset', - 'add_size', - 'header_data', - 'header_base', - 'header_offset', - 'salt', - 'volume_file', - ) + # rar5-only fields + blake2sp_hash = None + file_redir = None + + # internal fields + flags = 0 + type = None def isdir(self): - '''Returns True if the entry is a directory.''' + """Returns True if entry is a directory. + """ if self.type == RAR_BLOCK_FILE: return (self.flags & RAR_FILE_DIRECTORY) == RAR_FILE_DIRECTORY return False def needs_password(self): - return (self.flags & RAR_FILE_PASSWORD) > 0 + """Returns True if data is stored password-protected. + """ + if self.type == RAR_BLOCK_FILE: + return (self.flags & RAR_FILE_PASSWORD) > 0 + return False class RarFile(object): - '''Parse RAR structure, provide access to files in archive. - ''' + """Parse RAR structure, provide access to files in archive. + """ - #: Archive comment. Byte string or None. Use :data:`UNICODE_COMMENTS` - #: to get automatic decoding to unicode. + #: Archive comment. Unicode string or None. comment = None def __init__(self, rarfile, mode="r", charset=None, info_callback=None, - crc_check = True, errors = "stop"): + crc_check=True, errors="stop"): """Open and parse a RAR archive. - + Parameters: rarfile @@ -463,18 +646,12 @@ class RarFile(object): Either "stop" to quietly stop parsing on errors, or "strict" to raise errors. Default is "stop". """ - self.rarfile = rarfile - self.comment = None + self._rarfile = rarfile self._charset = charset or DEFAULT_CHARSET self._info_callback = info_callback - - self._info_list = [] - self._info_map = {} - self._parse_error = None - self._needs_password = False - self._password = None self._crc_check = crc_check - self._vol_list = [] + self._password = None + self._file_parser = None if errors == "stop": self._strict = False @@ -483,69 +660,62 @@ class RarFile(object): else: raise ValueError("Invalid value for 'errors' parameter.") - self._main = None - if mode != "r": raise NotImplementedError("RarFile supports only mode=r") self._parse() def __enter__(self): + """Open context.""" return self - def __exit__(self, type, value, traceback): + def __exit__(self, typ, value, traceback): + """Exit context""" self.close() def setpassword(self, password): - '''Sets the password to use when extracting.''' + """Sets the password to use when extracting. + """ self._password = password - if not self._main: + if self._file_parser: + if self._file_parser.has_header_encryption(): + self._file_parser = None + if not self._file_parser: self._parse() + else: + self._file_parser.setpassword(self._password) def needs_password(self): - '''Returns True if any archive entries require password for extraction.''' - return self._needs_password + """Returns True if any archive entries require password for extraction. + """ + return self._file_parser.needs_password() def namelist(self): - '''Return list of filenames in archive.''' + """Return list of filenames in archive. + """ return [f.filename for f in self.infolist()] def infolist(self): - '''Return RarInfo objects for all files/directories in archive.''' - return self._info_list + """Return RarInfo objects for all files/directories in archive. + """ + return self._file_parser.infolist() def volumelist(self): - '''Returns filenames of archive volumes. + """Returns filenames of archive volumes. In case of single-volume archive, the list contains just the name of main archive file. - ''' - return self._vol_list + """ + return self._file_parser.volumelist() def getinfo(self, fname): - '''Return RarInfo for file.''' + """Return RarInfo for file. + """ + return self._file_parser.getinfo(fname) - if isinstance(fname, RarInfo): - return fname + def open(self, fname, mode='r', psw=None): + """Returns file-like object (:class:`RarExtFile`) from where the data can be read. - # accept both ways here - if PATH_SEP == '/': - fname2 = fname.replace("\\", "/") - else: - fname2 = fname.replace("/", "\\") - - try: - return self._info_map[fname] - except KeyError: - try: - return self._info_map[fname2] - except KeyError: - raise NoRarEntry("No such file: "+fname) - - def open(self, fname, mode = 'r', psw = None): - '''Returns file-like object (:class:`RarExtFile`), - from where the data can be read. - The object implements :class:`io.RawIOBase` interface, so it can be further wrapped with :class:`io.BufferedReader` and :class:`io.TextIOWrapper`. @@ -565,7 +735,7 @@ class RarFile(object): must be 'r' psw password to use for extracting. - ''' + """ if mode != 'r': raise NotImplementedError("RarFile.open() supports only mode=r") @@ -575,9 +745,6 @@ class RarFile(object): if inf.isdir(): raise TypeError("Directory does not have any data: " + inf.filename) - if inf.flags & RAR_FILE_SPLIT_BEFORE: - raise NeedFirstVolume("Partial file, please start from first volume: " + inf.filename) - # check password if inf.needs_password(): psw = psw or self._password @@ -586,34 +753,11 @@ class RarFile(object): else: psw = None - # is temp write usable? - use_hack = 1 - if not self._main: - use_hack = 0 - elif self._main.flags & (RAR_MAIN_SOLID | RAR_MAIN_PASSWORD): - use_hack = 0 - elif inf.flags & (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER): - use_hack = 0 - elif is_filelike(self.rarfile): - pass - elif inf.file_size > HACK_SIZE_LIMIT: - use_hack = 0 - elif not USE_EXTRACT_HACK: - use_hack = 0 + return self._file_parser.open(inf, psw) - # now extract - if inf.compress_type == RAR_M0 and (inf.flags & RAR_FILE_PASSWORD) == 0: - return self._open_clear(inf) - elif use_hack: - return self._open_hack(inf, psw) - elif is_filelike(self.rarfile): - return self._open_unrar_membuf(self.rarfile, inf, psw) - else: - return self._open_unrar(self.rarfile, inf, psw) - - def read(self, fname, psw = None): + def read(self, fname, psw=None): """Return uncompressed data for archive entry. - + For longer files using :meth:`RarFile.open` may be better idea. Parameters: @@ -624,11 +768,8 @@ class RarFile(object): password to use for extracting. """ - f = self.open(fname, 'r', psw) - try: + with self.open(fname, 'r', psw) as f: return f.read() - finally: - f.close() def close(self): """Release open resources.""" @@ -641,7 +782,7 @@ class RarFile(object): def extract(self, member, path=None, pwd=None): """Extract single file into current directory. - + Parameters: member @@ -659,7 +800,7 @@ class RarFile(object): def extractall(self, path=None, members=None, pwd=None): """Extract all files into current directory. - + Parameters: path @@ -684,77 +825,149 @@ class RarFile(object): cmd = [UNRAR_TOOL] + list(TEST_ARGS) add_password_arg(cmd, self._password) cmd.append('--') - - if is_filelike(self.rarfile): - tmpname = membuf_tempfile(self.rarfile) - cmd.append(tmpname) - else: - tmpname = None - cmd.append(self.rarfile) - - try: + with XTempFile(self._rarfile) as rarfile: + cmd.append(rarfile) p = custom_popen(cmd) output = p.communicate()[0] check_returncode(p, output) - finally: - if tmpname: - os.unlink(tmpname) def strerror(self): - """Return error string if parsing failed, - or None if no problems. + """Return error string if parsing failed or None if no problems. """ - return self._parse_error + if not self._file_parser: + return "Not a RAR file" + return self._file_parser.strerror() ## ## private methods ## - def _set_error(self, msg, *args): - if args: - msg = msg % args - self._parse_error = msg - if self._strict: - raise BadRarFile(msg) + def _parse(self): + ver = _get_rar_version(self._rarfile) + if ver == 3: + p3 = RAR3Parser(self._rarfile, self._password, self._crc_check, + self._charset, self._strict, self._info_callback) + self._file_parser = p3 # noqa + elif ver == 5: + p5 = RAR5Parser(self._rarfile, self._password, self._crc_check, + self._charset, self._strict, self._info_callback) + self._file_parser = p5 # noqa + else: + raise BadRarFile("Not a RAR file") - # store entry - def _process_entry(self, item): - if item.type == RAR_BLOCK_FILE: - # use only first part - if (item.flags & RAR_FILE_SPLIT_BEFORE) == 0: - self._info_map[item.filename] = item - self._info_list.append(item) - # remember if any items require password - if item.needs_password(): - self._needs_password = True - elif len(self._info_list) > 0: - # final crc is in last block - old = self._info_list[-1] - old.CRC = item.CRC - old.compress_size += item.compress_size + self._file_parser.parse() + self.comment = self._file_parser.comment - # parse new-style comment - if item.type == RAR_BLOCK_SUB and item.filename == 'CMT': - if not NEED_COMMENTS: - pass - elif item.flags & (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER): - pass - elif item.flags & RAR_FILE_SOLID: - # file comment - cmt = self._read_comment_v3(item, self._password) - if len(self._info_list) > 0: - old = self._info_list[-1] - old.comment = cmt - else: - # archive comment - cmt = self._read_comment_v3(item, self._password) - self.comment = cmt + # call unrar to extract a file + def _extract(self, fnlist, path=None, psw=None): + cmd = [UNRAR_TOOL] + list(EXTRACT_ARGS) - if self._info_callback: - self._info_callback(item) + # pasoword + psw = psw or self._password + add_password_arg(cmd, psw) + cmd.append('--') + + # rar file + with XTempFile(self._rarfile) as rarfn: + cmd.append(rarfn) + + # file list + for fn in fnlist: + if os.sep != PATH_SEP: + fn = fn.replace(PATH_SEP, os.sep) + cmd.append(fn) + + # destination path + if path is not None: + cmd.append(path + os.sep) + + # call + p = custom_popen(cmd) + output = p.communicate()[0] + check_returncode(p, output) + +# +# File format parsing +# + +class CommonParser(object): + """Shared parser parts.""" + _main = None + _hdrenc_main = None + _needs_password = False + _fd = None + _expect_sig = None + _parse_error = None + _password = None + comment = None + + def __init__(self, rarfile, password, crc_check, charset, strict, info_cb): + self._rarfile = rarfile + self._password = password + self._crc_check = crc_check + self._charset = charset + self._strict = strict + self._info_callback = info_cb + self._info_list = [] + self._info_map = {} + self._vol_list = [] + + def has_header_encryption(self): + """Returns True if headers are encrypted + """ + if self._hdrenc_main: + return True + if self._main: + if self._main.flags & RAR_MAIN_PASSWORD: + return True + return False + + def setpassword(self, psw): + """Set cached password.""" + self._password = psw + + def volumelist(self): + """Volume files""" + return self._vol_list + + def needs_password(self): + """Is password required""" + return self._needs_password + + def strerror(self): + """Last error""" + return self._parse_error + + def infolist(self): + """List of RarInfo records. + """ + return self._info_list + + def getinfo(self, member): + """Return RarInfo for filename + """ + if isinstance(member, RarInfo): + fname = member.filename + else: + fname = member + + # accept both ways here + if PATH_SEP == '/': + fname2 = fname.replace("\\", "/") + else: + fname2 = fname.replace("/", "\\") + + try: + return self._info_map[fname] + except KeyError: + try: + return self._info_map[fname2] + except KeyError: + raise NoRarEntry("No such file: %s" % fname) # read rar - def _parse(self): + def parse(self): + """Process file.""" self._fd = None try: self._parse_real() @@ -764,19 +977,19 @@ class RarFile(object): self._fd = None def _parse_real(self): - fd = XFile(self.rarfile) + fd = XFile(self._rarfile) self._fd = fd - id = fd.read(len(RAR_ID)) - if id != RAR_ID: - if isinstance(self.rarfile, (str, unicode)): - raise NotRarFile("Not a Rar archive: {}".format(self.rarfile)) + sig = fd.read(len(self._expect_sig)) + if sig != self._expect_sig: + if isinstance(self._rarfile, (str, unicode)): + raise NotRarFile("Not a Rar archive: {}".format(self._rarfile)) raise NotRarFile("Not a Rar archive") volume = 0 # first vol (.rar) is 0 - more_vols = 0 - endarc = 0 - volfile = self.rarfile - self._vol_list = [self.rarfile] + more_vols = False + endarc = False + volfile = self._rarfile + self._vol_list = [self._rarfile] while 1: if endarc: h = None # don't read past ENDARC @@ -793,8 +1006,12 @@ class RarFile(object): self._set_error("Cannot open next volume: %s", volfile) break self._fd = fd - more_vols = 0 - endarc = 0 + sig = fd.read(len(self._expect_sig)) + if sig != self._expect_sig: + self._set_error("Invalid volume sig: %s", volfile) + break + more_vols = False + endarc = False self._vol_list.append(volfile) continue break @@ -811,28 +1028,206 @@ class RarFile(object): if h.flags & RAR_MAIN_PASSWORD: self._needs_password = True if not self._password: - self._main = None break elif h.type == RAR_BLOCK_ENDARC: - more_vols = h.flags & RAR_ENDARC_NEXT_VOLUME - endarc = 1 + more_vols = (h.flags & RAR_ENDARC_NEXT_VOLUME) > 0 + endarc = True elif h.type == RAR_BLOCK_FILE: # RAR 2.x does not write RAR_BLOCK_ENDARC if h.flags & RAR_FILE_SPLIT_AFTER: - more_vols = 1 + more_vols = True # RAR 2.x does not set RAR_MAIN_FIRSTVOLUME if volume == 0 and h.flags & RAR_FILE_SPLIT_BEFORE: raise NeedFirstVolume("Need to start from first volume") + if h.needs_password(): + self._needs_password = True + # store it - self._process_entry(h) + self.process_entry(fd, h) + + if self._info_callback: + self._info_callback(h) # go to next header if h.add_size > 0: - fd.seek(h.file_offset + h.add_size, 0) + fd.seek(h.data_offset + h.add_size, 0) + + def process_entry(self, fd, item): + """Examine item, add into lookup cache.""" + raise NotImplementedError() + + def _decrypt_header(self, fd): + raise NotImplementedError('_decrypt_header') + + def _parse_block_header(self, fd): + raise NotImplementedError('_parse_block_header') + + def _open_hack(self, inf, psw): + raise NotImplementedError('_open_hack') + + # read single header + def _parse_header(self, fd): + try: + # handle encrypted headers + if (self._main and self._main.flags & RAR_MAIN_PASSWORD) or self._hdrenc_main: + if not self._password: + return + fd = self._decrypt_header(fd) + + # now read actual header + return self._parse_block_header(fd) + except struct.error: + self._set_error('Broken header in RAR file') + return None + + # given current vol name, construct next one + def _next_volname(self, volfile): + if is_filelike(volfile): + raise IOError("Working on single FD") + if self._main.flags & RAR_MAIN_NEWNUMBERING: + return _next_newvol(volfile) + return _next_oldvol(volfile) + + def _set_error(self, msg, *args): + if args: + msg = msg % args + self._parse_error = msg + if self._strict: + raise BadRarFile(msg) + + def open(self, inf, psw): + """Return stream object for file data.""" + + if inf.file_redir: + # cannot leave to unrar as it expects copied file to exist + if inf.file_redir[0] in (RAR5_XREDIR_FILE_COPY, RAR5_XREDIR_HARD_LINK): + inf = self.getinfo(inf.file_redir[2]) + if not inf: + raise BadRarFile('cannot find copied file') + + if inf.flags & RAR_FILE_SPLIT_BEFORE: + raise NeedFirstVolume("Partial file, please start from first volume: " + inf.filename) + + # is temp write usable? + use_hack = 1 + if not self._main: + use_hack = 0 + elif self._main._must_disable_hack(): + use_hack = 0 + elif inf._must_disable_hack(): + use_hack = 0 + elif is_filelike(self._rarfile): + pass + elif inf.file_size > HACK_SIZE_LIMIT: + use_hack = 0 + elif not USE_EXTRACT_HACK: + use_hack = 0 + + # now extract + if inf.compress_type == RAR_M0 and (inf.flags & RAR_FILE_PASSWORD) == 0 and inf.file_redir is None: + return self._open_clear(inf) + elif use_hack: + return self._open_hack(inf, psw) + elif is_filelike(self._rarfile): + return self._open_unrar_membuf(self._rarfile, inf, psw) + else: + return self._open_unrar(self._rarfile, inf, psw) + + def _open_clear(self, inf): + return DirectReader(self, inf) + + def _open_hack_core(self, inf, psw, prefix, suffix): + + size = inf.compress_size + inf.header_size + rf = XFile(inf.volume_file, 0) + rf.seek(inf.header_offset) + + tmpfd, tmpname = mkstemp(suffix='.rar') + tmpf = os.fdopen(tmpfd, "wb") + + try: + tmpf.write(prefix) + while size > 0: + if size > BSIZE: + buf = rf.read(BSIZE) + else: + buf = rf.read(size) + if not buf: + raise BadRarFile('read failed: ' + inf.filename) + tmpf.write(buf) + size -= len(buf) + tmpf.write(suffix) + tmpf.close() + rf.close() + except: + rf.close() + tmpf.close() + os.unlink(tmpname) + raise + + return self._open_unrar(tmpname, inf, psw, tmpname) + + # write in-memory archive to temp file - needed for solid archives + def _open_unrar_membuf(self, memfile, inf, psw): + tmpname = membuf_tempfile(memfile) + return self._open_unrar(tmpname, inf, psw, tmpname, force_file=True) + + # extract using unrar + def _open_unrar(self, rarfile, inf, psw=None, tmpfile=None, force_file=False): + cmd = [UNRAR_TOOL] + list(OPEN_ARGS) + add_password_arg(cmd, psw) + cmd.append("--") + cmd.append(rarfile) + + # not giving filename avoids encoding related problems + if not tmpfile or force_file: + fn = inf.filename + if PATH_SEP != os.sep: + fn = fn.replace(PATH_SEP, os.sep) + cmd.append(fn) + + # read from unrar pipe + return PipeReader(self, inf, cmd, tmpfile) + +# +# RAR3 format +# + +class Rar3Info(RarInfo): + """RAR3 specific fields.""" + extract_version = 15 + salt = None + add_size = 0 + header_crc = None + header_size = None + header_offset = None + data_offset = None + _md_class = None + _md_expect = None + + # make sure some rar5 fields are always present + file_redir = None + blake2sp_hash = None + + def _must_disable_hack(self): + if self.type == RAR_BLOCK_FILE: + if self.flags & RAR_FILE_PASSWORD: + return True + elif self.flags & (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER): + return True + elif self.type == RAR_BLOCK_MAIN: + if self.flags & (RAR_MAIN_SOLID | RAR_MAIN_PASSWORD): + return True + return False + + +class RAR3Parser(CommonParser): + """Parse RAR3 file format. + """ + _expect_sig = RAR_ID + _last_aes_key = (None, None, None) # (salt, key, iv) - # AES encrypted headers - _last_aes_key = (None, None, None) # (salt, key, iv) def _decrypt_header(self, fd): if not _have_crypto: raise NoCrypto('Cannot parse encrypted headers - no crypto') @@ -844,26 +1239,10 @@ class RarFile(object): self._last_aes_key = (salt, key, iv) return HeaderDecrypt(fd, key, iv) - # read single header - def _parse_header(self, fd): - try: - # handle encrypted headers - if self._main and self._main.flags & RAR_MAIN_PASSWORD: - if not self._password: - return - fd = self._decrypt_header(fd) - - # now read actual header - return self._parse_block_header(fd) - except struct.error: - self._set_error('Broken header in RAR file') - return None - # common header def _parse_block_header(self, fd): - h = RarInfo() + h = Rar3Info() h.header_offset = fd.tell() - h.comment = None # read and parse base header buf = fd.read(S_BLK_HDR.size) @@ -871,24 +1250,24 @@ class RarFile(object): return None t = S_BLK_HDR.unpack_from(buf) h.header_crc, h.type, h.flags, h.header_size = t - h.header_base = S_BLK_HDR.size - pos = S_BLK_HDR.size # read full header if h.header_size > S_BLK_HDR.size: - h.header_data = buf + fd.read(h.header_size - S_BLK_HDR.size) + hdata = buf + fd.read(h.header_size - S_BLK_HDR.size) else: - h.header_data = buf - h.file_offset = fd.tell() + hdata = buf + h.data_offset = fd.tell() # unexpected EOF? - if len(h.header_data) != h.header_size: + if len(hdata) != h.header_size: self._set_error('Unexpected EOF when reading header') return None + pos = S_BLK_HDR.size + # block has data assiciated with it? if h.flags & RAR_LONG_BLOCK: - h.add_size = S_LONG.unpack_from(h.header_data, pos)[0] + h.add_size, pos = load_le32(hdata, pos) else: h.add_size = 0 @@ -896,31 +1275,36 @@ class RarFile(object): if h.type == RAR_BLOCK_MARK: return h elif h.type == RAR_BLOCK_MAIN: - h.header_base += 6 + pos += 6 if h.flags & RAR_MAIN_ENCRYPTVER: - h.header_base += 1 + pos += 1 + crc_pos = pos if h.flags & RAR_MAIN_COMMENT: - self._parse_subblocks(h, h.header_base) - self.comment = h.comment + self._parse_subblocks(h, hdata, pos) elif h.type == RAR_BLOCK_FILE: - self._parse_file_header(h, pos) + pos = self._parse_file_header(h, hdata, pos - 4) + crc_pos = pos + if h.flags & RAR_FILE_COMMENT: + pos = self._parse_subblocks(h, hdata, pos) elif h.type == RAR_BLOCK_SUB: - self._parse_file_header(h, pos) - h.header_base = h.header_size + pos = self._parse_file_header(h, hdata, pos - 4) + crc_pos = h.header_size elif h.type == RAR_BLOCK_OLD_AUTH: - h.header_base += 8 + pos += 8 + crc_pos = pos elif h.type == RAR_BLOCK_OLD_EXTRA: - h.header_base += 7 + pos += 7 + crc_pos = pos else: - h.header_base = h.header_size + crc_pos = h.header_size # check crc if h.type == RAR_BLOCK_OLD_SUB: - crcdat = h.header_data[2:] + fd.read(h.add_size) + crcdat = hdata[2:] + fd.read(h.add_size) else: - crcdat = h.header_data[2:h.header_base] + crcdat = hdata[2:crc_pos] - calc_crc = crc32(crcdat) & 0xFFFF + calc_crc = rar_crc32(crcdat) & 0xFFFF # return good header if h.header_crc == calc_crc: @@ -928,39 +1312,42 @@ class RarFile(object): # header parsing failed. self._set_error('Header CRC error (%02x): exp=%x got=%x (xlen = %d)', - h.type, h.header_crc, calc_crc, len(crcdat)) + h.type, h.header_crc, calc_crc, len(crcdat)) # instead panicing, send eof return None # read file-specific header - def _parse_file_header(self, h, pos): - fld = S_FILE_HDR.unpack_from(h.header_data, pos) + def _parse_file_header(self, h, hdata, pos): + fld = S_FILE_HDR.unpack_from(hdata, pos) + pos += S_FILE_HDR.size + h.compress_size = fld[0] h.file_size = fld[1] h.host_os = fld[2] h.CRC = fld[3] h.date_time = parse_dos_time(fld[4]) + h.mtime = to_datetime(h.date_time) h.extract_version = fld[5] h.compress_type = fld[6] - h.name_size = fld[7] + name_size = fld[7] h.mode = fld[8] - pos += S_FILE_HDR.size + + h._md_class = CRC32Context + h._md_expect = h.CRC if h.flags & RAR_FILE_LARGE: - h1 = S_LONG.unpack_from(h.header_data, pos)[0] - h2 = S_LONG.unpack_from(h.header_data, pos + 4)[0] + h1, pos = load_le32(hdata, pos) + h2, pos = load_le32(hdata, pos) h.compress_size |= h1 << 32 h.file_size |= h2 << 32 - pos += 8 h.add_size = h.compress_size - name = h.header_data[pos : pos + h.name_size ] - pos += h.name_size + name, pos = load_bytes(hdata, name_size, pos) if h.flags & RAR_FILE_UNICODE: nul = name.find(ZERO) h.orig_filename = name[:nul] - u = UnicodeFilename(h.orig_filename, name[nul + 1 : ]) + u = UnicodeFilename(h.orig_filename, name[nul + 1:]) h.filename = u.decode() # if parsing failed fall back to simple name @@ -975,48 +1362,24 @@ class RarFile(object): h.filename = h.filename.replace('\\', PATH_SEP) if h.flags & RAR_FILE_SALT: - h.salt = h.header_data[pos : pos + 8] - pos += 8 + h.salt, pos = load_bytes(hdata, 8, pos) else: h.salt = None # optional extended time stamps if h.flags & RAR_FILE_EXTTIME: - pos = self._parse_ext_time(h, pos) + pos = _parse_ext_time(h, hdata, pos) else: h.mtime = h.atime = h.ctime = h.arctime = None - # base header end - h.header_base = pos - - if h.flags & RAR_FILE_COMMENT: - self._parse_subblocks(h, pos) - - # convert timestamps - if USE_DATETIME: - h.date_time = to_datetime(h.date_time) - h.mtime = to_datetime(h.mtime) - h.atime = to_datetime(h.atime) - h.ctime = to_datetime(h.ctime) - h.arctime = to_datetime(h.arctime) - - # .mtime is .date_time with more precision - if h.mtime: - if USE_DATETIME: - h.date_time = h.mtime - else: - # keep seconds int - h.date_time = h.mtime[:5] + (int(h.mtime[5]),) - return pos # find old-style comment subblock - def _parse_subblocks(self, h, pos): - hdata = h.header_data + def _parse_subblocks(self, h, hdata, pos): while pos < len(hdata): # ordinary block header t = S_BLK_HDR.unpack_from(hdata, pos) - scrc, stype, sflags, slen = t + ___scrc, stype, sflags, slen = t pos_next = pos + slen pos += S_BLK_HDR.size @@ -1029,168 +1392,35 @@ class RarFile(object): declen, ver, meth, crc = S_COMMENT_HDR.unpack_from(hdata, pos) pos += S_COMMENT_HDR.size data = hdata[pos : pos_next] - cmt = rar_decompress(ver, meth, data, declen, sflags, - crc, self._password) + cmt = rar3_decompress(ver, meth, data, declen, sflags, + crc, self._password) if not self._crc_check: h.comment = self._decode_comment(cmt) - elif crc32(cmt) & 0xFFFF == crc: + elif rar_crc32(cmt) & 0xFFFF == crc: h.comment = self._decode_comment(cmt) pos = pos_next - - def _parse_ext_time(self, h, pos): - data = h.header_data - - # flags and rest of data can be missing - flags = 0 - if pos + 2 <= len(data): - flags = S_SHORT.unpack_from(data, pos)[0] - pos += 2 - - h.mtime, pos = self._parse_xtime(flags >> 3*4, data, pos, h.date_time) - h.ctime, pos = self._parse_xtime(flags >> 2*4, data, pos) - h.atime, pos = self._parse_xtime(flags >> 1*4, data, pos) - h.arctime, pos = self._parse_xtime(flags >> 0*4, data, pos) return pos - def _parse_xtime(self, flag, data, pos, dostime = None): - unit = 10000000.0 # 100 ns units - if flag & 8: - if not dostime: - t = S_LONG.unpack_from(data, pos)[0] - dostime = parse_dos_time(t) - pos += 4 - rem = 0 - cnt = flag & 3 - for i in range(cnt): - b = S_BYTE.unpack_from(data, pos)[0] - rem = (b << 16) | (rem >> 8) - pos += 1 - sec = dostime[5] + rem / unit - if flag & 4: - sec += 1 - dostime = dostime[:5] + (sec,) - return dostime, pos - - # given current vol name, construct next one - def _next_volname(self, volfile): - if is_filelike(volfile): - raise IOError("Working on single FD") - if self._main.flags & RAR_MAIN_NEWNUMBERING: - return self._next_newvol(volfile) - return self._next_oldvol(volfile) - - # new-style next volume - def _next_newvol(self, volfile): - i = len(volfile) - 1 - while i >= 0: - if volfile[i] >= '0' and volfile[i] <= '9': - return self._inc_volname(volfile, i) - i -= 1 - raise BadRarName("Cannot construct volume name: "+volfile) - - # old-style next volume - def _next_oldvol(self, volfile): - # rar -> r00 - if volfile[-4:].lower() == '.rar': - return volfile[:-2] + '00' - return self._inc_volname(volfile, len(volfile) - 1) - - # increase digits with carry, otherwise just increment char - def _inc_volname(self, volfile, i): - fn = list(volfile) - while i >= 0: - if fn[i] != '9': - fn[i] = chr(ord(fn[i]) + 1) - break - fn[i] = '0' - i -= 1 - return ''.join(fn) - - def _open_clear(self, inf): - return DirectReader(self, inf) - - # put file compressed data into temporary .rar archive, and run - # unrar on that, thus avoiding unrar going over whole archive - def _open_hack(self, inf, psw = None): - BSIZE = 32*1024 - - size = inf.compress_size + inf.header_size - rf = XFile(inf.volume_file, 0) - rf.seek(inf.header_offset) - - tmpfd, tmpname = mkstemp(suffix='.rar') - tmpf = os.fdopen(tmpfd, "wb") - - try: - # create main header: crc, type, flags, size, res1, res2 - mh = S_BLK_HDR.pack(0x90CF, 0x73, 0, 13) + ZERO * (2+4) - tmpf.write(RAR_ID + mh) - while size > 0: - if size > BSIZE: - buf = rf.read(BSIZE) - else: - buf = rf.read(size) - if not buf: - raise BadRarFile('read failed: ' + inf.filename) - tmpf.write(buf) - size -= len(buf) - tmpf.close() - rf.close() - except: - rf.close() - tmpf.close() - os.unlink(tmpname) - raise - - return self._open_unrar(tmpname, inf, psw, tmpname) - def _read_comment_v3(self, inf, psw=None): # read data - rf = XFile(inf.volume_file) - rf.seek(inf.file_offset) - data = rf.read(inf.compress_size) - rf.close() + with XFile(inf.volume_file) as rf: + rf.seek(inf.data_offset) + data = rf.read(inf.compress_size) # decompress - cmt = rar_decompress(inf.extract_version, inf.compress_type, data, - inf.file_size, inf.flags, inf.CRC, psw, inf.salt) + cmt = rar3_decompress(inf.extract_version, inf.compress_type, data, + inf.file_size, inf.flags, inf.CRC, psw, inf.salt) # check crc if self._crc_check: - crc = crc32(cmt) - if crc < 0: - crc += (1 << 32) + crc = rar_crc32(cmt) if crc != inf.CRC: return None return self._decode_comment(cmt) - # write in-memory archive to temp file - needed for solid archives - def _open_unrar_membuf(self, memfile, inf, psw): - tmpname = membuf_tempfile(memfile) - return self._open_unrar(tmpname, inf, psw, tmpname) - - # extract using unrar - def _open_unrar(self, rarfile, inf, psw = None, tmpfile = None): - if is_filelike(rarfile): - raise ValueError("Cannot use unrar directly on memory buffer") - cmd = [UNRAR_TOOL] + list(OPEN_ARGS) - add_password_arg(cmd, psw) - cmd.append("--") - cmd.append(rarfile) - - # not giving filename avoids encoding related problems - if not tmpfile: - fn = inf.filename - if PATH_SEP != os.sep: - fn = fn.replace(PATH_SEP, os.sep) - cmd.append(fn) - - # read from unrar pipe - return PipeReader(self, inf, cmd, tmpfile) - def _decode(self, val): for c in TRY_ENCODINGS: try: @@ -1200,53 +1430,466 @@ class RarFile(object): return val.decode(self._charset, 'replace') def _decode_comment(self, val): - if UNICODE_COMMENTS: - return self._decode(val) - return val + return self._decode(val) - # call unrar to extract a file - def _extract(self, fnlist, path=None, psw=None): - cmd = [UNRAR_TOOL] + list(EXTRACT_ARGS) + def process_entry(self, fd, item): + if item.type == RAR_BLOCK_FILE: + # use only first part + if (item.flags & RAR_FILE_SPLIT_BEFORE) == 0: + self._info_map[item.filename] = item + self._info_list.append(item) + elif len(self._info_list) > 0: + # final crc is in last block + old = self._info_list[-1] + old.CRC = item.CRC + old._md_expect = item._md_expect + old.compress_size += item.compress_size - # pasoword - psw = psw or self._password - add_password_arg(cmd, psw) - cmd.append('--') + # parse new-style comment + if item.type == RAR_BLOCK_SUB and item.filename == 'CMT': + if item.flags & (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER): + pass + elif item.flags & RAR_FILE_SOLID: + # file comment + cmt = self._read_comment_v3(item, self._password) + if len(self._info_list) > 0: + old = self._info_list[-1] + old.comment = cmt + else: + # archive comment + cmt = self._read_comment_v3(item, self._password) + self.comment = cmt - # rar file - if is_filelike(self.rarfile): - tmpname = membuf_tempfile(self.rarfile) - cmd.append(tmpname) + if item.type == RAR_BLOCK_MAIN: + if item.flags & RAR_MAIN_COMMENT: + self.comment = item.comment + if item.flags & RAR_MAIN_PASSWORD: + self._needs_password = True + + # put file compressed data into temporary .rar archive, and run + # unrar on that, thus avoiding unrar going over whole archive + def _open_hack(self, inf, psw): + # create main header: crc, type, flags, size, res1, res2 + prefix = RAR_ID + S_BLK_HDR.pack(0x90CF, 0x73, 0, 13) + ZERO * (2 + 4) + return self._open_hack_core(inf, psw, prefix, EMPTY) + +# +# RAR5 format +# + +class Rar5Info(RarInfo): + """Shared fields for RAR5 records. + """ + extract_version = 50 + header_crc = None + header_size = None + header_offset = None + data_offset = None + + # type=all + block_type = None + block_flags = None + add_size = 0 + block_extra_size = 0 + + # type=MAIN + volume_number = None + _md_class = None + _md_expect = None + + def _must_disable_hack(self): + return False + + +class Rar5BaseFile(Rar5Info): + """Shared sturct for file & service record. + """ + type = -1 + file_flags = None + file_encryption = (0, 0, 0, EMPTY, EMPTY, EMPTY) + file_compress_flags = None + file_redir = None + file_owner = None + file_version = None + blake2sp_hash = None + + def _must_disable_hack(self): + if self.flags & RAR_FILE_PASSWORD: + return True + if self.block_flags & (RAR5_BLOCK_FLAG_SPLIT_BEFORE | RAR5_BLOCK_FLAG_SPLIT_AFTER): + return True + if self.file_compress_flags & RAR5_COMPR_SOLID: + return True + if self.file_redir: + return True + return False + + +class Rar5FileInfo(Rar5BaseFile): + """RAR5 file record. + """ + type = RAR_BLOCK_FILE + + +class Rar5ServiceInfo(Rar5BaseFile): + """RAR5 service record. + """ + type = RAR_BLOCK_SUB + + +class Rar5MainInfo(Rar5Info): + """RAR5 archive main record. + """ + type = RAR_BLOCK_MAIN + main_flags = None + main_volume_number = None + + def _must_disable_hack(self): + if self.main_flags & RAR5_MAIN_FLAG_SOLID: + return True + return False + + +class Rar5EncryptionInfo(Rar5Info): + """RAR5 archive header encryption record. + """ + type = RAR5_BLOCK_ENCRYPTION + encryption_algo = None + encryption_flags = None + encryption_kdf_count = None + encryption_salt = None + encryption_check_value = None + + def needs_password(self): + return True + + +class Rar5EndArcInfo(Rar5Info): + """RAR5 end of archive record. + """ + type = RAR_BLOCK_ENDARC + endarc_flags = None + + +class RAR5Parser(CommonParser): + """Parse RAR5 format. + """ + _expect_sig = RAR5_ID + _hdrenc_main = None + + # AES encrypted headers + _last_aes256_key = (-1, None, None) # (kdf_count, salt, key) + + def _gen_key(self, kdf_count, salt): + if self._last_aes256_key[:2] == (kdf_count, salt): + return self._last_aes256_key[2] + if kdf_count > 24: + raise BadRarFile('Too large kdf_count') + psw = self._password + if isinstance(psw, unicode): + psw = psw.encode('utf8') + key = pbkdf2_sha256(psw, salt, 1 << kdf_count) + self._last_aes256_key = (kdf_count, salt, key) + return key + + def _decrypt_header(self, fd): + if not _have_crypto: + raise NoCrypto('Cannot parse encrypted headers - no crypto') + h = self._hdrenc_main + key = self._gen_key(h.encryption_kdf_count, h.encryption_salt) + iv = fd.read(16) + return HeaderDecrypt(fd, key, iv) + + # common header + def _parse_block_header(self, fd): + header_offset = fd.tell() + + preload = 4 + 3 + start_bytes = fd.read(preload) + header_crc, pos = load_le32(start_bytes, 0) + hdrlen, pos = load_vint(start_bytes, pos) + if hdrlen > 2 * 1024 * 1024: + return None + header_size = pos + hdrlen + + # read full header, check for EOF + hdata = start_bytes + fd.read(header_size - len(start_bytes)) + if len(hdata) != header_size: + self._set_error('Unexpected EOF when reading header') + return None + data_offset = fd.tell() + + calc_crc = rar_crc32(memoryview(hdata)[4:]) + if header_crc != calc_crc: + # header parsing failed. + self._set_error('Header CRC error: exp=%x got=%x (xlen = %d)', + header_crc, calc_crc, len(hdata)) + return None + + block_type, pos = load_vint(hdata, pos) + + if block_type == RAR5_BLOCK_MAIN: + h, pos = self._parse_block_common(Rar5MainInfo(), hdata) + h = self._parse_main_block(h, hdata, pos) + elif block_type == RAR5_BLOCK_FILE: + h, pos = self._parse_block_common(Rar5FileInfo(), hdata) + h = self._parse_file_block(h, hdata, pos) + elif block_type == RAR5_BLOCK_SERVICE: + h, pos = self._parse_block_common(Rar5ServiceInfo(), hdata) + h = self._parse_file_block(h, hdata, pos) + elif block_type == RAR5_BLOCK_ENCRYPTION: + h, pos = self._parse_block_common(Rar5EncryptionInfo(), hdata) + h = self._parse_encryption_block(h, hdata, pos) + elif block_type == RAR5_BLOCK_ENDARC: + h, pos = self._parse_block_common(Rar5EndArcInfo(), hdata) + h = self._parse_endarc_block(h, hdata, pos) else: - tmpname = None - cmd.append(self.rarfile) + h = None + if h: + h.header_offset = header_offset + h.data_offset = data_offset + return h - # file list - for fn in fnlist: - if os.sep != PATH_SEP: - fn = fn.replace(PATH_SEP, os.sep) - cmd.append(fn) + def _parse_block_common(self, h, hdata): + h.header_crc, pos = load_le32(hdata, 0) + hdrlen, pos = load_vint(hdata, pos) + h.header_size = hdrlen + pos + h.block_type, pos = load_vint(hdata, pos) + h.block_flags, pos = load_vint(hdata, pos) - # destination path - if path is not None: - cmd.append(path + os.sep) + if h.block_flags & RAR5_BLOCK_FLAG_EXTRA_DATA: + h.block_extra_size, pos = load_vint(hdata, pos) + if h.block_flags & RAR5_BLOCK_FLAG_DATA_AREA: + h.add_size, pos = load_vint(hdata, pos) - # call - try: - p = custom_popen(cmd) - output = p.communicate()[0] - check_returncode(p, output) - finally: - if tmpname: - os.unlink(tmpname) + h.compress_size = h.add_size + + if h.block_flags & RAR5_BLOCK_FLAG_SKIP_IF_UNKNOWN: + h.flags |= RAR_SKIP_IF_UNKNOWN + if h.block_flags & RAR5_BLOCK_FLAG_DATA_AREA: + h.flags |= RAR_LONG_BLOCK + return h, pos + + def _parse_main_block(self, h, hdata, pos): + h.main_flags, pos = load_vint(hdata, pos) + if h.main_flags & RAR5_MAIN_FLAG_HAS_VOLNR: + h.main_volume_number = load_vint(hdata, pos) + + h.flags |= RAR_MAIN_NEWNUMBERING + if h.main_flags & RAR5_MAIN_FLAG_SOLID: + h.flags |= RAR_MAIN_SOLID + if h.main_flags & RAR5_MAIN_FLAG_ISVOL: + h.flags |= RAR_MAIN_VOLUME + if h.main_flags & RAR5_MAIN_FLAG_RECOVERY: + h.flags |= RAR_MAIN_RECOVERY + if self._hdrenc_main: + h.flags |= RAR_MAIN_PASSWORD + if h.main_flags & RAR5_MAIN_FLAG_HAS_VOLNR == 0: + h.flags |= RAR_MAIN_FIRSTVOLUME + + return h + + def _parse_file_block(self, h, hdata, pos): + h.file_flags, pos = load_vint(hdata, pos) + h.file_size, pos = load_vint(hdata, pos) + h.mode, pos = load_vint(hdata, pos) + + if h.file_flags & RAR5_FILE_FLAG_HAS_MTIME: + h.mtime, pos = load_unixtime(hdata, pos) + h.date_time = h.mtime.timetuple()[:6] + if h.file_flags & RAR5_FILE_FLAG_HAS_CRC32: + h.CRC, pos = load_le32(hdata, pos) + h._md_class = CRC32Context + h._md_expect = h.CRC + + h.file_compress_flags, pos = load_vint(hdata, pos) + h.file_host_os, pos = load_vint(hdata, pos) + h.orig_filename, pos = load_vstr(hdata, pos) + h.filename = h.orig_filename.decode('utf8', 'replace') + + # use compatible values + if h.file_host_os == RAR5_OS_WINDOWS: + h.host_os = RAR_OS_WIN32 + else: + h.host_os = RAR_OS_UNIX + h.compress_type = RAR_M0 + ((h.file_compress_flags >> 7) & 7) + + if h.block_extra_size: + # allow 1 byte of garbage + while pos < len(hdata) - 1: + xsize, pos = load_vint(hdata, pos) + xdata, pos = load_bytes(hdata, xsize, pos) + self._process_file_extra(h, xdata) + + if h.block_flags & RAR5_BLOCK_FLAG_SPLIT_BEFORE: + h.flags |= RAR_FILE_SPLIT_BEFORE + if h.block_flags & RAR5_BLOCK_FLAG_SPLIT_AFTER: + h.flags |= RAR_FILE_SPLIT_AFTER + if h.file_flags & RAR5_FILE_FLAG_ISDIR: + h.flags |= RAR_FILE_DIRECTORY + if h.file_compress_flags & RAR5_COMPR_SOLID: + h.flags |= RAR_FILE_SOLID + + return h + + def _parse_endarc_block(self, h, hdata, pos): + h.endarc_flags, pos = load_vint(hdata, pos) + if h.endarc_flags & RAR5_ENDARC_FLAG_NEXT_VOL: + h.flags |= RAR_ENDARC_NEXT_VOLUME + return h + + def _parse_encryption_block(self, h, hdata, pos): + h.encryption_algo, pos = load_vint(hdata, pos) + h.encryption_flags, pos = load_vint(hdata, pos) + h.encryption_kdf_count, pos = load_byte(hdata, pos) + h.encryption_salt, pos = load_bytes(hdata, 16, pos) + if h.encryption_flags & RAR5_ENC_FLAG_HAS_CHECKVAL: + h.encryption_check_value = load_bytes(hdata, 12, pos) + if h.encryption_algo != RAR5_XENC_CIPHER_AES256: + raise BadRarFile('Unsupported header encryption cipher') + self._hdrenc_main = h + return h + + # file extra record + def _process_file_extra(self, h, xdata): + xtype, pos = load_vint(xdata, 0) + if xtype == RAR5_XFILE_TIME: + self._parse_file_xtime(h, xdata, pos) + elif xtype == RAR5_XFILE_ENCRYPTION: + self._parse_file_encryption(h, xdata, pos) + elif xtype == RAR5_XFILE_HASH: + self._parse_file_hash(h, xdata, pos) + elif xtype == RAR5_XFILE_VERSION: + self._parse_file_version(h, xdata, pos) + elif xtype == RAR5_XFILE_REDIR: + self._parse_file_redir(h, xdata, pos) + elif xtype == RAR5_XFILE_OWNER: + self._parse_file_owner(h, xdata, pos) + elif xtype == RAR5_XFILE_SERVICE: + pass + else: + pass + + # extra block for file time record + def _parse_file_xtime(self, h, xdata, pos): + tflags, pos = load_vint(xdata, pos) + ldr = load_windowstime + if tflags & RAR5_XTIME_UNIXTIME: + ldr = load_unixtime + if tflags & RAR5_XTIME_HAS_MTIME: + h.mtime, pos = ldr(xdata, pos) + h.date_time = h.mtime.timetuple()[:6] + if tflags & RAR5_XTIME_HAS_CTIME: + h.ctime, pos = ldr(xdata, pos) + if tflags & RAR5_XTIME_HAS_ATIME: + h.atime, pos = ldr(xdata, pos) + + # just remember encryption info + def _parse_file_encryption(self, h, xdata, pos): + algo, pos = load_vint(xdata, pos) + flags, pos = load_vint(xdata, pos) + kdf_count, pos = load_byte(xdata, pos) + salt, pos = load_bytes(xdata, 16, pos) + iv, pos = load_bytes(xdata, 16, pos) + checkval = None + if flags & RAR5_XENC_CHECKVAL: + checkval, pos = load_bytes(xdata, 12, pos) + if flags & RAR5_XENC_TWEAKED: + h._md_expect = None + h._md_class = NoHashContext + + h.file_encryption = (algo, flags, kdf_count, salt, iv, checkval) + h.flags |= RAR_FILE_PASSWORD + + def _parse_file_hash(self, h, xdata, pos): + hash_type, pos = load_vint(xdata, pos) + if hash_type == RAR5_XHASH_BLAKE2SP: + h.blake2sp_hash, pos = load_bytes(xdata, 32, pos) + if _have_blake2 and (h.file_encryption[1] & RAR5_XENC_TWEAKED) == 0: + h._md_class = Blake2SP + h._md_expect = h.blake2sp_hash + + def _parse_file_version(self, h, xdata, pos): + flags, pos = load_vint(xdata, pos) + version, pos = load_vint(xdata, pos) + h.file_version = (flags, version) + + def _parse_file_redir(self, h, xdata, pos): + redir_type, pos = load_vint(xdata, pos) + redir_flags, pos = load_vint(xdata, pos) + redir_name, pos = load_vstr(xdata, pos) + redir_name = redir_name.decode('utf8', 'replace') + h.file_redir = (redir_type, redir_flags, redir_name) + + def _parse_file_owner(self, h, xdata, pos): + user_name = group_name = user_id = group_id = None + + flags, pos = load_vint(xdata, pos) + if flags & RAR5_XOWNER_UNAME: + user_name, pos = load_vstr(xdata, pos) + if flags & RAR5_XOWNER_GNAME: + group_name, pos = load_vstr(xdata, pos) + if flags & RAR5_XOWNER_UID: + user_id, pos = load_vint(xdata, pos) + if flags & RAR5_XOWNER_GID: + group_id, pos = load_vint(xdata, pos) + + h.file_owner = (user_name, group_name, user_id, group_id) + + def process_entry(self, fd, item): + if item.block_type == RAR5_BLOCK_FILE: + # use only first part + if (item.block_flags & RAR5_BLOCK_FLAG_SPLIT_BEFORE) == 0: + self._info_map[item.filename] = item + self._info_list.append(item) + elif len(self._info_list) > 0: + # final crc is in last block + old = self._info_list[-1] + old.CRC = item.CRC + old._md_expect = item._md_expect + old.blake2sp_hash = item.blake2sp_hash + old.compress_size += item.compress_size + elif item.block_type == RAR5_BLOCK_SERVICE: + if item.filename == 'CMT': + self._load_comment(fd, item) + + def _load_comment(self, fd, item): + if item.block_flags & (RAR5_BLOCK_FLAG_SPLIT_BEFORE | RAR5_BLOCK_FLAG_SPLIT_AFTER): + return None + if item.compress_type != RAR_M0: + return None + + if item.flags & RAR_FILE_PASSWORD: + algo, ___flags, kdf_count, salt, iv, ___checkval = item.file_encryption + if algo != RAR5_XENC_CIPHER_AES256: + return None + key = self._gen_key(kdf_count, salt) + f = HeaderDecrypt(fd, key, iv) + cmt = f.read(item.file_size) + else: + # archive comment + with self._open_clear(item) as cmtstream: + cmt = cmtstream.read() + + # rar bug? - appends zero to comment + cmt = cmt.split(ZERO, 1)[0] + self.comment = cmt.decode('utf8') + + def _open_hack(self, inf, psw): + # len, type, blk_flags, flags + main_hdr = b'\x03\x01\x00\x00' + endarc_hdr = b'\x03\x05\x00\x00' + main_hdr = S_LONG.pack(rar_crc32(main_hdr)) + main_hdr + endarc_hdr = S_LONG.pack(rar_crc32(endarc_hdr)) + endarc_hdr + return self._open_hack_core(inf, psw, RAR5_ID + main_hdr, endarc_hdr) ## ## Utility classes ## class UnicodeFilename(object): - """Handle unicode filename decompression""" - + """Handle RAR3 unicode filename decompression. + """ def __init__(self, name, encdata): self.std_name = bytearray(name) self.encdata = bytearray(encdata) @@ -1255,6 +1898,7 @@ class UnicodeFilename(object): self.failed = 0 def enc_byte(self): + """Copy encoded byte.""" try: c = self.encdata[self.encpos] self.encpos += 1 @@ -1264,6 +1908,7 @@ class UnicodeFilename(object): return 0 def std_byte(self): + """Copy byte from 8-bit representation.""" try: return self.std_name[self.pos] except IndexError: @@ -1271,11 +1916,13 @@ class UnicodeFilename(object): return ord('?') def put(self, lo, hi): + """Copy 16-bit value to result.""" self.buf.append(lo) self.buf.append(hi) self.pos += 1 def decode(self): + """Decompress compressed UTF16 value.""" hi = self.enc_byte() flagbits = 0 while self.encpos < len(self.encdata): @@ -1294,11 +1941,11 @@ class UnicodeFilename(object): n = self.enc_byte() if n & 0x80: c = self.enc_byte() - for i in range((n & 0x7f) + 2): + for _ in range((n & 0x7f) + 2): lo = (self.std_byte() + c) & 0xFF self.put(lo, hi) else: - for i in range(n + 2): + for _ in range(n + 2): self.put(self.std_byte(), 0) return self.buf.decode("utf-16le", "replace") @@ -1311,77 +1958,78 @@ class RarExtFile(RawIOBase): Behaviour: - no short reads - .read() and .readinfo() read as much as requested. - no internal buffer, use io.BufferedReader for that. - - If :mod:`io` module is available (Python 2.6+, 3.x), then this calls - will inherit from :class:`io.RawIOBase` class. This makes line-based - access available: :meth:`RarExtFile.readline` and ``for ln in f``. """ #: Filename of the archive entry name = None - def __init__(self, rf, inf): + def __init__(self, parser, inf): + """Open archive entry. + """ super(RarExtFile, self).__init__() # standard io.* properties self.name = inf.filename self.mode = 'rb' - self.rf = rf - self.inf = inf - self.crc_check = rf._crc_check - self.fd = None - self.CRC = 0 - self.remain = 0 - self.returncode = 0 + self._parser = parser + self._inf = inf + self._fd = None + self._remain = 0 + self._returncode = 0 + + self._md_context = None self._open() def _open(self): - if self.fd: - self.fd.close() - self.fd = None - self.CRC = 0 - self.remain = self.inf.file_size + if self._fd: + self._fd.close() + md_class = self._inf._md_class or NoHashContext + self._md_context = md_class() + self._fd = None + self._remain = self._inf.file_size - def read(self, cnt = None): + def read(self, cnt=None): """Read all or specified amount of data from archive entry.""" # sanitize cnt if cnt is None or cnt < 0: - cnt = self.remain - elif cnt > self.remain: - cnt = self.remain + cnt = self._remain + elif cnt > self._remain: + cnt = self._remain if cnt == 0: return EMPTY # actual read data = self._read(cnt) if data: - self.CRC = crc32(data, self.CRC) - self.remain -= len(data) + self._md_context.update(data) + self._remain -= len(data) if len(data) != cnt: raise BadRarFile("Failed the read enough data") # done? - if not data or self.remain == 0: - #self.close() + if not data or self._remain == 0: + # self.close() self._check() return data def _check(self): """Check final CRC.""" - if not self.crc_check: + final = self._md_context.digest() + exp = self._inf._md_expect + if exp is None: return - if self.returncode: + if final is None: + return + if self._returncode: check_returncode(self, '') - if self.remain != 0: + if self._remain != 0: raise BadRarFile("Failed the read enough data") - crc = self.CRC - if crc < 0: - crc += (1 << 32) - if crc != self.inf.CRC: - raise BadRarFile("Corrupt file - CRC check failed: " + self.inf.filename) + if final != exp: + raise BadRarFile("Corrupt file - CRC check failed: %s - exp=%r got=%r" % ( + self._inf.filename, exp, final)) def _read(self, cnt): """Actual read that gets sanitized cnt.""" @@ -1391,9 +2039,9 @@ class RarExtFile(RawIOBase): super(RarExtFile, self).close() - if self.fd: - self.fd.close() - self.fd = None + if self._fd: + self._fd.close() + self._fd = None def __del__(self): """Hook delete to make sure tempfile is removed.""" @@ -1404,25 +2052,15 @@ class RarExtFile(RawIOBase): Returns bytes read. """ - - data = self.read(len(buf)) - n = len(data) - try: - buf[:n] = data - except TypeError: - import array - if not isinstance(buf, array.array): - raise - buf[:n] = array.array(buf.typecode, data) - return n + raise NotImplementedError('readinto') def tell(self): """Return current reading position in uncompressed data.""" - return self.inf.file_size - self.remain + return self._inf.file_size - self._remain - def seek(self, ofs, whence = 0): + def seek(self, ofs, whence=0): """Seek in data. - + On uncompressed files, the seeking works by actual seeks so it's fast. On compresses files its slow - forward seeking happends by reading ahead, @@ -1430,9 +2068,9 @@ class RarExtFile(RawIOBase): """ # disable crc check when seeking - self.crc_check = 0 + self._md_context = NoHashContext() - fsize = self.inf.file_size + fsize = self._inf.file_size cur_ofs = self.tell() if whence == 0: # seek from beginning of file @@ -1454,8 +2092,6 @@ class RarExtFile(RawIOBase): if new_ofs >= cur_ofs: self._skip(new_ofs - cur_ofs) else: - # process old data ? - #self._skip(fsize - cur_ofs) # reopen and seek self._open() self._skip(new_ofs) @@ -1478,13 +2114,14 @@ class RarExtFile(RawIOBase): def writable(self): """Returns False. - - Writing is not supported.""" + + Writing is not supported. + """ return False def seekable(self): """Returns True. - + Seeking is supported, although it's slow on compressed files. """ return True @@ -1499,23 +2136,23 @@ class PipeReader(RarExtFile): """Read data from pipe, handle tempfile cleanup.""" def __init__(self, rf, inf, cmd, tempfile=None): - self.cmd = cmd - self.proc = None - self.tempfile = tempfile + self._cmd = cmd + self._proc = None + self._tempfile = tempfile super(PipeReader, self).__init__(rf, inf) def _close_proc(self): - if not self.proc: + if not self._proc: return - if self.proc.stdout: - self.proc.stdout.close() - if self.proc.stdin: - self.proc.stdin.close() - if self.proc.stderr: - self.proc.stderr.close() - self.proc.wait() - self.returncode = self.proc.returncode - self.proc = None + if self._proc.stdout: + self._proc.stdout.close() + if self._proc.stdin: + self._proc.stdin.close() + if self._proc.stderr: + self._proc.stderr.close() + self._proc.wait() + self._returncode = self._proc.returncode + self._proc = None def _open(self): super(PipeReader, self)._open() @@ -1524,19 +2161,19 @@ class PipeReader(RarExtFile): self._close_proc() # launch new process - self.returncode = 0 - self.proc = custom_popen(self.cmd) - self.fd = self.proc.stdout + self._returncode = 0 + self._proc = custom_popen(self._cmd) + self._fd = self._proc.stdout # avoid situation where unrar waits on stdin - if self.proc.stdin: - self.proc.stdin.close() + if self._proc.stdin: + self._proc.stdin.close() def _read(self, cnt): """Read from pipe.""" # normal read is usually enough - data = self.fd.read(cnt) + data = self._fd.read(cnt) if len(data) == cnt or not data: return data @@ -1544,7 +2181,7 @@ class PipeReader(RarExtFile): buf = [data] cnt -= len(data) while cnt > 0: - data = self.fd.read(cnt) + data = self._fd.read(cnt) if not data: break cnt -= len(data) @@ -1557,42 +2194,45 @@ class PipeReader(RarExtFile): self._close_proc() super(PipeReader, self).close() - if self.tempfile: + if self._tempfile: try: - os.unlink(self.tempfile) + os.unlink(self._tempfile) except OSError: pass - self.tempfile = None + self._tempfile = None def readinto(self, buf): """Zero-copy read directly into buffer.""" cnt = len(buf) - if cnt > self.remain: - cnt = self.remain + if cnt > self._remain: + cnt = self._remain vbuf = memoryview(buf) res = got = 0 while got < cnt: - res = self.fd.readinto(vbuf[got : cnt]) + res = self._fd.readinto(vbuf[got : cnt]) if not res: break - if self.crc_check: - self.CRC = crc32(vbuf[got : got + res], self.CRC) - self.remain -= res + self._md_context.update(vbuf[got : got + res]) + self._remain -= res got += res return got class DirectReader(RarExtFile): - """Read uncompressed data directly from archive.""" + """Read uncompressed data directly from archive. + """ + _cur = None + _cur_avail = None + _volfile = None def _open(self): super(DirectReader, self)._open() - self.volfile = self.inf.volume_file - self.fd = XFile(self.volfile, 0) - self.fd.seek(self.inf.header_offset, 0) - self.cur = self.rf._parse_header(self.fd) - self.cur_avail = self.cur.add_size + self._volfile = self._inf.volume_file + self._fd = XFile(self._volfile, 0) + self._fd.seek(self._inf.header_offset, 0) + self._cur = self._parser._parse_header(self._fd) + self._cur_avail = self._cur.add_size def _skip(self, cnt): """RAR Seek, skipping through rar files to get to correct position @@ -1600,19 +2240,19 @@ class DirectReader(RarExtFile): while cnt > 0: # next vol needed? - if self.cur_avail == 0: + if self._cur_avail == 0: if not self._open_next(): break # fd is in read pos, do the read - if cnt > self.cur_avail: - cnt -= self.cur_avail - self.remain -= self.cur_avail - self.cur_avail = 0 + if cnt > self._cur_avail: + cnt -= self._cur_avail + self._remain -= self._cur_avail + self._cur_avail = 0 else: - self.fd.seek(cnt, 1) - self.cur_avail -= cnt - self.remain -= cnt + self._fd.seek(cnt, 1) + self._cur_avail -= cnt + self._remain -= cnt cnt = 0 def _read(self, cnt): @@ -1621,21 +2261,21 @@ class DirectReader(RarExtFile): buf = [] while cnt > 0: # next vol needed? - if self.cur_avail == 0: + if self._cur_avail == 0: if not self._open_next(): break # fd is in read pos, do the read - if cnt > self.cur_avail: - data = self.fd.read(self.cur_avail) + if cnt > self._cur_avail: + data = self._fd.read(self._cur_avail) else: - data = self.fd.read(cnt) + data = self._fd.read(cnt) if not data: break # got some data cnt -= len(data) - self.cur_avail -= len(data) + self._cur_avail -= len(data) buf.append(data) if len(buf) == 1: @@ -1646,31 +2286,34 @@ class DirectReader(RarExtFile): """Proceed to next volume.""" # is the file split over archives? - if (self.cur.flags & RAR_FILE_SPLIT_AFTER) == 0: + if (self._cur.flags & RAR_FILE_SPLIT_AFTER) == 0: return False - if self.fd: - self.fd.close() - self.fd = None + if self._fd: + self._fd.close() + self._fd = None # open next part - self.volfile = self.rf._next_volname(self.volfile) - fd = open(self.volfile, "rb", 0) - self.fd = fd + self._volfile = self._parser._next_volname(self._volfile) + fd = open(self._volfile, "rb", 0) + self._fd = fd + sig = fd.read(len(self._parser._expect_sig)) + if sig != self._parser._expect_sig: + raise BadRarFile("Invalid signature") # loop until first file header while 1: - cur = self.rf._parse_header(fd) + cur = self._parser._parse_header(fd) if not cur: raise BadRarFile("Unexpected EOF") if cur.type in (RAR_BLOCK_MARK, RAR_BLOCK_MAIN): if cur.add_size: fd.seek(cur.add_size, 1) continue - if cur.orig_filename != self.inf.orig_filename: + if cur.orig_filename != self._inf.orig_filename: raise BadRarFile("Did not found file entry") - self.cur = cur - self.cur_avail = cur.add_size + self._cur = cur + self._cur_avail = cur.add_size return True def readinto(self, buf): @@ -1679,23 +2322,22 @@ class DirectReader(RarExtFile): vbuf = memoryview(buf) while got < len(buf): # next vol needed? - if self.cur_avail == 0: + if self._cur_avail == 0: if not self._open_next(): break # length for next read cnt = len(buf) - got - if cnt > self.cur_avail: - cnt = self.cur_avail + if cnt > self._cur_avail: + cnt = self._cur_avail # read into temp view - res = self.fd.readinto(vbuf[got : got + cnt]) + res = self._fd.readinto(vbuf[got : got + cnt]) if not res: break - if self.crc_check: - self.CRC = crc32(vbuf[got : got + res], self.CRC) - self.cur_avail -= res - self.remain -= res + self._md_context.update(vbuf[got : got + res]) + self._cur_avail -= res + self._remain -= res got += res return got @@ -1708,10 +2350,12 @@ class HeaderDecrypt(object): self.buf = EMPTY def tell(self): + """Current file pos - works only on block boundaries.""" return self.f.tell() def read(self, cnt=None): - if cnt > 8*1024: + """Read and decrypt.""" + if cnt > 8 * 1024: raise BadRarFile('Bad count to header decrypt - wrong password?') # consume old data @@ -1724,10 +2368,10 @@ class HeaderDecrypt(object): cnt -= len(res) # decrypt new data - BLK = self.ciph.block_size + blklen = 16 while cnt > 0: - enc = self.f.read(BLK) - if len(enc) < BLK: + enc = self.f.read(blklen) + if len(enc) < blklen: break dec = self.ciph.decrypt(enc) if cnt >= len(dec): @@ -1740,10 +2384,14 @@ class HeaderDecrypt(object): return res + # handle (filename|filelike) object class XFile(object): + """Input may be filename or file object. + """ __slots__ = ('_fd', '_need_close') - def __init__(self, xfile, bufsize = 1024): + + def __init__(self, xfile, bufsize=1024): if is_filelike(xfile): self._need_close = False self._fd = xfile @@ -1751,27 +2399,279 @@ class XFile(object): else: self._need_close = True self._fd = open(xfile, 'rb', bufsize) + def read(self, n=None): + """Read from file.""" return self._fd.read(n) + def tell(self): + """Return file pos.""" return self._fd.tell() + def seek(self, ofs, whence=0): + """Move file pos.""" return self._fd.seek(ofs, whence) + def readinto(self, dst): + """Read into buffer.""" return self._fd.readinto(dst) + def close(self): + """Close file object.""" if self._need_close: self._fd.close() + def __enter__(self): return self + def __exit__(self, typ, val, tb): self.close() + +class NoHashContext(object): + """No-op hash function.""" + def __init__(self, data=None): + """Initialize""" + def update(self, data): + """Update data""" + def digest(self): + """Final hash""" + def hexdigest(self): + """Hexadecimal digest.""" + + +class CRC32Context(object): + """Hash context that uses CRC32.""" + __slots__ = ['_crc'] + + def __init__(self, data=None): + self._crc = 0 + if data: + self.update(data) + + def update(self, data): + """Process data.""" + self._crc = rar_crc32(data, self._crc) + + def digest(self): + """Final hash.""" + return self._crc + + def hexdigest(self): + """Hexadecimal digest.""" + return '%08x' % self.digest() + + +class Blake2SP(object): + """Blake2sp hash context. + """ + __slots__ = ['_thread', '_buf', '_cur', '_digest'] + digest_size = 32 + block_size = 64 + parallelism = 8 + + def __init__(self, data=None): + self._buf = b'' + self._cur = 0 + self._digest = None + self._thread = [] + + for i in range(self.parallelism): + ctx = self._blake2s(i, 0, i == (self.parallelism - 1)) + self._thread.append(ctx) + + if data: + self.update(data) + + def _blake2s(self, ofs, depth, is_last): + return blake2s(node_offset=ofs, node_depth=depth, last_node=is_last, + depth=2, inner_size=32, fanout=self.parallelism) + + def _add_block(self, blk): + self._thread[self._cur].update(blk) + self._cur = (self._cur + 1) % self.parallelism + + def update(self, data): + """Hash data. + """ + view = memoryview(data) + bs = self.block_size + if self._buf: + need = bs - len(self._buf) + if len(view) < need: + self._buf += view.tobytes() + return + self._add_block(self._buf + view[:need].tobytes()) + view = view[need:] + while len(view) >= bs: + self._add_block(view[:bs]) + view = view[bs:] + self._buf = view.tobytes() + + def digest(self): + """Return final digest value. + """ + if self._digest is None: + if self._buf: + self._add_block(self._buf) + self._buf = EMPTY + ctx = self._blake2s(0, 1, True) + for t in self._thread: + ctx.update(t.digest()) + self._digest = ctx.digest() + return self._digest + + def hexdigest(self): + """Hexadecimal digest.""" + return tohex(self.digest()) + ## ## Utility functions ## +S_LONG = Struct(' len(buf): + raise BadRarFile('cannot load byte') + return S_BYTE.unpack_from(buf, pos)[0], end + +def load_le32(buf, pos): + """Load little-endian 32-bit integer""" + end = pos + 4 + if end > len(buf): + raise BadRarFile('cannot load le32') + return S_LONG.unpack_from(buf, pos)[0], pos + 4 + +def load_bytes(buf, num, pos): + """Load sequence of bytes""" + end = pos + num + if end > len(buf): + raise BadRarFile('cannot load bytes') + return buf[pos : end], end + +def load_vstr(buf, pos): + """Load bytes prefixed by vint length""" + slen, pos = load_vint(buf, pos) + return load_bytes(buf, slen, pos) + +def load_dostime(buf, pos): + """Load LE32 dos timestamp""" + stamp, pos = load_le32(buf, pos) + tup = parse_dos_time(stamp) + return to_datetime(tup), pos + +def load_unixtime(buf, pos): + """Load LE32 unix timestamp""" + secs, pos = load_le32(buf, pos) + dt = datetime.fromtimestamp(secs, UTC) + return dt, pos + +def load_windowstime(buf, pos): + """Load LE64 windows timestamp""" + # unix epoch (1970) in seconds from windows epoch (1601) + unix_epoch = 11644473600 + val1, pos = load_le32(buf, pos) + val2, pos = load_le32(buf, pos) + secs, n1secs = divmod((val2 << 32) | val1, 10000000) + dt = datetime.fromtimestamp(secs - unix_epoch, UTC) + dt = dt.replace(microsecond=n1secs // 10) + return dt, pos + +# new-style next volume +def _next_newvol(volfile): + i = len(volfile) - 1 + while i >= 0: + if volfile[i] >= '0' and volfile[i] <= '9': + return _inc_volname(volfile, i) + i -= 1 + raise BadRarName("Cannot construct volume name: " + volfile) + +# old-style next volume +def _next_oldvol(volfile): + # rar -> r00 + if volfile[-4:].lower() == '.rar': + return volfile[:-2] + '00' + return _inc_volname(volfile, len(volfile) - 1) + +# increase digits with carry, otherwise just increment char +def _inc_volname(volfile, i): + fn = list(volfile) + while i >= 0: + if fn[i] != '9': + fn[i] = chr(ord(fn[i]) + 1) + break + fn[i] = '0' + i -= 1 + return ''.join(fn) + +# rar3 extended time fields +def _parse_ext_time(h, data, pos): + # flags and rest of data can be missing + flags = 0 + if pos + 2 <= len(data): + flags = S_SHORT.unpack_from(data, pos)[0] + pos += 2 + + mtime, pos = _parse_xtime(flags >> 3 * 4, data, pos, h.mtime) + h.ctime, pos = _parse_xtime(flags >> 2 * 4, data, pos) + h.atime, pos = _parse_xtime(flags >> 1 * 4, data, pos) + h.arctime, pos = _parse_xtime(flags >> 0 * 4, data, pos) + if mtime: + h.mtime = mtime + h.date_time = mtime.timetuple()[:6] + return pos + +# rar3 one extended time field +def _parse_xtime(flag, data, pos, basetime=None): + res = None + if flag & 8: + if not basetime: + basetime, pos = load_dostime(data, pos) + + # load second fractions + rem = 0 + cnt = flag & 3 + for _ in range(cnt): + b, pos = load_byte(data, pos) + rem = (b << 16) | (rem >> 8) + + # convert 100ns units to microseconds + usec = rem // 10 + if usec > 1000000: + usec = 999999 + + # dostime has room for 30 seconds only, correct if needed + if flag & 4 and basetime.second < 59: + res = basetime.replace(microsecond=usec, second=basetime.second + 1) + else: + res = basetime.replace(microsecond=usec) + return res, pos + def is_filelike(obj): + """Filename or file object? + """ if isinstance(obj, str) or isinstance(obj, unicode): return False res = True @@ -1782,14 +2682,16 @@ def is_filelike(obj): return True def rar3_s2k(psw, salt): - """String-to-key hash for RAR3.""" - + """String-to-key hash for RAR3. + """ + if not isinstance(psw, unicode): + psw = psw.decode('utf8') seed = psw.encode('utf-16le') + salt iv = EMPTY h = sha1() for i in range(16): for j in range(0x4000): - cnt = S_LONG.pack(i*0x4000 + j) + cnt = S_LONG.pack(i * 0x4000 + j) h.update(seed + cnt[:3]) if j == 0: iv += h.digest()[19:20] @@ -1797,12 +2699,11 @@ def rar3_s2k(psw, salt): key_le = pack("LLLL", key_be)) return key_le, iv -def rar_decompress(vers, meth, data, declen=0, flags=0, crc=0, psw=None, salt=None): +def rar3_decompress(vers, meth, data, declen=0, flags=0, crc=0, psw=None, salt=None): """Decompress blob of compressed data. Used for data with non-standard header - eg. comments. """ - # already uncompressed? if meth == RAR_M0 and (flags & RAR_FILE_PASSWORD) == 0: return data @@ -1826,11 +2727,11 @@ def rar_decompress(vers, meth, data, declen=0, flags=0, crc=0, psw=None, salt=No # full header hlen = S_BLK_HDR.size + len(fhdr) hdr = S_BLK_HDR.pack(0, RAR_BLOCK_FILE, flags, hlen) + fhdr - hcrc = crc32(hdr[2:]) & 0xFFFF + hcrc = rar_crc32(hdr[2:]) & 0xFFFF hdr = S_BLK_HDR.pack(hcrc, RAR_BLOCK_FILE, flags, hlen) + fhdr # archive main header - mh = S_BLK_HDR.pack(0x90CF, RAR_BLOCK_MAIN, 0, 13) + ZERO * (2+4) + mh = S_BLK_HDR.pack(0x90CF, RAR_BLOCK_MAIN, 0, 13) + ZERO * (2 + 4) # decompress via temp rar tmpfd, tmpname = mkstemp(suffix='.rar') @@ -1850,62 +2751,66 @@ def rar_decompress(vers, meth, data, declen=0, flags=0, crc=0, psw=None, salt=No os.unlink(tmpname) def to_datetime(t): - """Convert 6-part time tuple into datetime object.""" - + """Convert 6-part time tuple into datetime object. + """ if t is None: return None # extract values - year, mon, day, h, m, xs = t - s = int(xs) - us = int(1000000 * (xs - s)) + year, mon, day, h, m, s = t # assume the values are valid try: - return datetime(year, mon, day, h, m, s, us) + return datetime(year, mon, day, h, m, s) except ValueError: pass # sanitize invalid values - MDAY = (0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) - if mon < 1: mon = 1 - if mon > 12: mon = 12 - if day < 1: day = 1 - if day > MDAY[mon]: day = MDAY[mon] - if h > 23: h = 23 - if m > 59: m = 59 - if s > 59: s = 59 + mday = (0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) + if mon < 1: + mon = 1 + if mon > 12: + mon = 12 + if day < 1: + day = 1 + if day > mday[mon]: + day = mday[mon] + if h > 23: + h = 23 + if m > 59: + m = 59 + if s > 59: + s = 59 if mon == 2 and day == 29: try: - return datetime(year, mon, day, h, m, s, us) + return datetime(year, mon, day, h, m, s) except ValueError: day = 28 - return datetime(year, mon, day, h, m, s, us) + return datetime(year, mon, day, h, m, s) def parse_dos_time(stamp): - """Parse standard 32-bit DOS timestamp.""" - - sec = stamp & 0x1F; stamp = stamp >> 5 - min = stamp & 0x3F; stamp = stamp >> 6 - hr = stamp & 0x1F; stamp = stamp >> 5 - day = stamp & 0x1F; stamp = stamp >> 5 - mon = stamp & 0x0F; stamp = stamp >> 4 + """Parse standard 32-bit DOS timestamp. + """ + sec, stamp = stamp & 0x1F, stamp >> 5 + mn, stamp = stamp & 0x3F, stamp >> 6 + hr, stamp = stamp & 0x1F, stamp >> 5 + day, stamp = stamp & 0x1F, stamp >> 5 + mon, stamp = stamp & 0x0F, stamp >> 4 yr = (stamp & 0x7F) + 1980 - return (yr, mon, day, hr, min, sec * 2) + return (yr, mon, day, hr, mn, sec * 2) def custom_popen(cmd): - """Disconnect cmd from parent fds, read only from stdout.""" - + """Disconnect cmd from parent fds, read only from stdout. + """ # needed for py2exe creationflags = 0 if sys.platform == 'win32': - creationflags = 0x08000000 # CREATE_NO_WINDOW + creationflags = 0x08000000 # CREATE_NO_WINDOW # run command try: - p = Popen(cmd, bufsize = 0, - stdout = PIPE, stdin = PIPE, stderr = STDOUT, - creationflags = creationflags) + p = Popen(cmd, bufsize=0, stdout=PIPE, stdin=PIPE, stderr=STDOUT, + creationflags=creationflags) except OSError as ex: if ex.errno == errno.ENOENT: raise RarCannotExec("Unrar not installed? (rarfile.UNRAR_TOOL=%r)" % UNRAR_TOOL) @@ -1913,15 +2818,17 @@ def custom_popen(cmd): return p def custom_check(cmd, ignore_retcode=False): - """Run command, collect output, raise error if needed.""" + """Run command, collect output, raise error if needed. + """ p = custom_popen(cmd) - out, err = p.communicate() + out, _ = p.communicate() if p.returncode and not ignore_retcode: raise RarExecError("Check-run failed") return out -def add_password_arg(cmd, psw, required=False): - """Append password switch to commandline.""" +def add_password_arg(cmd, psw, ___required=False): + """Append password switch to commandline. + """ if UNRAR_TOOL == ALT_TOOL: return if psw is not None: @@ -1930,17 +2837,17 @@ def add_password_arg(cmd, psw, required=False): cmd.append('-p-') def check_returncode(p, out): - """Raise exception according to unrar exit code""" - + """Raise exception according to unrar exit code. + """ code = p.returncode if code == 0: return - # map return code to exception class + # map return code to exception class, codes from rar.txt errmap = [None, - RarWarning, RarFatalError, RarCRCError, RarLockedArchiveError, - RarWriteError, RarOpenError, RarUserError, RarMemoryError, - RarCreateError, RarNoFilesError] # codes from rar.txt + RarWarning, RarFatalError, RarCRCError, RarLockedArchiveError, # 1..4 + RarWriteError, RarOpenError, RarUserError, RarMemoryError, # 5..8 + RarCreateError, RarNoFilesError, RarWrongPassword] # 9..11 if UNRAR_TOOL == ALT_TOOL: errmap = [None] if code > 0 and code < len(errmap): @@ -1960,43 +2867,85 @@ def check_returncode(p, out): raise exc(msg) +def hmac_sha256(key, data): + """HMAC-SHA256""" + return HMAC(key, data, sha256).digest() + def membuf_tempfile(memfile): + """Write in-memory file object to real file.""" memfile.seek(0, 0) tmpfd, tmpname = mkstemp(suffix='.rar') tmpf = os.fdopen(tmpfd, "wb") try: - BSIZE = 32*1024 while True: buf = memfile.read(BSIZE) if not buf: break tmpf.write(buf) tmpf.close() - return tmpname except: tmpf.close() os.unlink(tmpname) raise + return tmpname + +class XTempFile(object): + """Real file for archive. + """ + __slots__ = ('_tmpfile', '_filename') + + def __init__(self, rarfile): + if is_filelike(rarfile): + self._tmpfile = membuf_tempfile(rarfile) + self._filename = self._tmpfile + else: + self._tmpfile = None + self._filename = rarfile + + def __enter__(self): + return self._filename + + def __exit__(self, exc_type, exc_value, tb): + if self._tmpfile: + try: + os.unlink(self._tmpfile) + except OSError: + pass + self._tmpfile = None # # Check if unrar works # -try: - # does UNRAR_TOOL work? - custom_check([UNRAR_TOOL], True) -except RarCannotExec: - try: - # does ALT_TOOL work? - custom_check([ALT_TOOL] + list(ALT_CHECK_ARGS), True) - # replace config - UNRAR_TOOL = ALT_TOOL - OPEN_ARGS = ALT_OPEN_ARGS - EXTRACT_ARGS = ALT_EXTRACT_ARGS - TEST_ARGS = ALT_TEST_ARGS - except RarCannotExec: - # no usable tool, only uncompressed archives work - pass +ORIG_UNRAR_TOOL = UNRAR_TOOL +ORIG_OPEN_ARGS = OPEN_ARGS +ORIG_EXTRACT_ARGS = EXTRACT_ARGS +ORIG_TEST_ARGS = TEST_ARGS + +def _check_unrar_tool(): + global UNRAR_TOOL, OPEN_ARGS, EXTRACT_ARGS, TEST_ARGS + try: + # does UNRAR_TOOL work? + custom_check([ORIG_UNRAR_TOOL], True) + + UNRAR_TOOL = ORIG_UNRAR_TOOL + OPEN_ARGS = ORIG_OPEN_ARGS + EXTRACT_ARGS = ORIG_EXTRACT_ARGS + TEST_ARGS = ORIG_TEST_ARGS + except RarCannotExec: + try: + # does ALT_TOOL work? + custom_check([ALT_TOOL] + list(ALT_CHECK_ARGS), True) + # replace config + UNRAR_TOOL = ALT_TOOL + OPEN_ARGS = ALT_OPEN_ARGS + EXTRACT_ARGS = ALT_EXTRACT_ARGS + TEST_ARGS = ALT_TEST_ARGS + except RarCannotExec: + # no usable tool, only uncompressed archives work + pass + +_check_unrar_tool() diff --git a/libs/stevedore/__init__.py b/libs/stevedore/__init__.py index 93a56b2e..a471f31d 100755 --- a/libs/stevedore/__init__.py +++ b/libs/stevedore/__init__.py @@ -20,17 +20,5 @@ import logging # the app we're used from does not set up logging. LOG = logging.getLogger('stevedore') -if hasattr(logging, 'NullHandler'): - LOG.addHandler(logging.NullHandler()) -else: - class NullHandler(logging.Handler): - def handle(self, record): - pass +LOG.addHandler(logging.NullHandler()) - def emit(self, record): - pass - - def createLock(self): - self.lock = None - - LOG.addHandler(NullHandler()) diff --git a/libs/stevedore/dispatch.py b/libs/stevedore/dispatch.py index 226d3ae2..a1589673 100755 --- a/libs/stevedore/dispatch.py +++ b/libs/stevedore/dispatch.py @@ -1,6 +1,19 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import logging from .enabled import EnabledExtensionManager +from .exception import NoMatches LOG = logging.getLogger(__name__) @@ -66,7 +79,7 @@ class DispatchExtensionManager(EnabledExtensionManager): """ if not self.extensions: # FIXME: Use a more specific exception class here. - raise RuntimeError('No %s extensions found' % self.namespace) + raise NoMatches('No %s extensions found' % self.namespace) response = [] for e in self.extensions: if filter_func(e, *args, **kwds): diff --git a/libs/stevedore/driver.py b/libs/stevedore/driver.py index 47273d06..167dc671 100755 --- a/libs/stevedore/driver.py +++ b/libs/stevedore/driver.py @@ -1,3 +1,16 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from .exception import NoMatches, MultipleMatches from .named import NamedExtensionManager @@ -27,12 +40,16 @@ class DriverManager(NamedExtensionManager): :param verify_requirements: Use setuptools to enforce the dependencies of the plugin(s) being loaded. Defaults to False. :type verify_requirements: bool + :type warn_on_missing_entrypoint: bool """ def __init__(self, namespace, name, invoke_on_load=False, invoke_args=(), invoke_kwds={}, on_load_failure_callback=None, - verify_requirements=False): + verify_requirements=False, + warn_on_missing_entrypoint=True): + on_load_failure_callback = on_load_failure_callback \ + or self._default_on_load_failure super(DriverManager, self).__init__( namespace=namespace, names=[name], @@ -41,8 +58,13 @@ class DriverManager(NamedExtensionManager): invoke_kwds=invoke_kwds, on_load_failure_callback=on_load_failure_callback, verify_requirements=verify_requirements, + warn_on_missing_entrypoint=warn_on_missing_entrypoint ) + @staticmethod + def _default_on_load_failure(drivermanager, ep, err): + raise + @classmethod def make_test_instance(cls, extension, namespace='TESTING', propagate_map_exceptions=False, @@ -87,14 +109,14 @@ class DriverManager(NamedExtensionManager): if not self.extensions: name = self._names[0] - raise RuntimeError('No %r driver found, looking for %r' % - (self.namespace, name)) + raise NoMatches('No %r driver found, looking for %r' % + (self.namespace, name)) if len(self.extensions) > 1: discovered_drivers = ','.join(e.entry_point_target for e in self.extensions) - raise RuntimeError('Multiple %r drivers found: %s' % - (self.namespace, discovered_drivers)) + raise MultipleMatches('Multiple %r drivers found: %s' % + (self.namespace, discovered_drivers)) def __call__(self, func, *args, **kwds): """Invokes func() for the single loaded extension. diff --git a/libs/stevedore/enabled.py b/libs/stevedore/enabled.py index 0d228db4..c2e0c03d 100755 --- a/libs/stevedore/enabled.py +++ b/libs/stevedore/enabled.py @@ -1,3 +1,15 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import logging from .extension import ExtensionManager @@ -16,7 +28,8 @@ class EnabledExtensionManager(ExtensionManager): :param namespace: The namespace for the entry points. :type namespace: str :param check_func: Function to determine which extensions to load. - :type check_func: callable + :type check_func: callable, taking an :class:`Extension` + instance as argument :param invoke_on_load: Boolean controlling whether to invoke the object returned by the entry point after the driver is loaded. :type invoke_on_load: bool diff --git a/libs/stevedore/example/__init__.py b/libs/stevedore/example/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/stevedore/example/base.py b/libs/stevedore/example/base.py new file mode 100644 index 00000000..ec95424e --- /dev/null +++ b/libs/stevedore/example/base.py @@ -0,0 +1,22 @@ +import abc + +import six + + +@six.add_metaclass(abc.ABCMeta) +class FormatterBase(object): + """Base class for example plugin used in the tutorial. + """ + + def __init__(self, max_width=60): + self.max_width = max_width + + @abc.abstractmethod + def format(self, data): + """Format the data and return unicode text. + + :param data: A dictionary with string keys and simple types as + values. + :type data: dict(str:?) + :returns: Iterable producing the formatted text. + """ diff --git a/libs/stevedore/example/load_as_driver.py b/libs/stevedore/example/load_as_driver.py new file mode 100644 index 00000000..d8c47f5f --- /dev/null +++ b/libs/stevedore/example/load_as_driver.py @@ -0,0 +1,37 @@ +from __future__ import print_function + +import argparse + +from stevedore import driver + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument( + 'format', + nargs='?', + default='simple', + help='the output format', + ) + parser.add_argument( + '--width', + default=60, + type=int, + help='maximum output width for text', + ) + parsed_args = parser.parse_args() + + data = { + 'a': 'A', + 'b': 'B', + 'long': 'word ' * 80, + } + + mgr = driver.DriverManager( + namespace='stevedore.example.formatter', + name=parsed_args.format, + invoke_on_load=True, + invoke_args=(parsed_args.width,), + ) + for chunk in mgr.driver.format(data): + print(chunk, end='') diff --git a/libs/stevedore/example/load_as_extension.py b/libs/stevedore/example/load_as_extension.py new file mode 100644 index 00000000..436206a3 --- /dev/null +++ b/libs/stevedore/example/load_as_extension.py @@ -0,0 +1,39 @@ +from __future__ import print_function + +import argparse + +from stevedore import extension + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument( + '--width', + default=60, + type=int, + help='maximum output width for text', + ) + parsed_args = parser.parse_args() + + data = { + 'a': 'A', + 'b': 'B', + 'long': 'word ' * 80, + } + + mgr = extension.ExtensionManager( + namespace='stevedore.example.formatter', + invoke_on_load=True, + invoke_args=(parsed_args.width,), + ) + + def format_data(ext, data): + return (ext.name, ext.obj.format(data)) + + results = mgr.map(format_data, data) + + for name, result in results: + print('Formatter: {0}'.format(name)) + for chunk in result: + print(chunk, end='') + print('') diff --git a/libs/stevedore/example/setup.py b/libs/stevedore/example/setup.py new file mode 100644 index 00000000..702e6d4d --- /dev/null +++ b/libs/stevedore/example/setup.py @@ -0,0 +1,43 @@ +from setuptools import setup, find_packages + +setup( + name='stevedore-examples', + version='1.0', + + description='Demonstration package for stevedore', + + author='Doug Hellmann', + author_email='doug@doughellmann.com', + + url='http://git.openstack.org/cgit/openstack/stevedore', + + classifiers=['Development Status :: 3 - Alpha', + 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.5', + 'Intended Audience :: Developers', + 'Environment :: Console', + ], + + platforms=['Any'], + + scripts=[], + + provides=['stevedore.examples', + ], + + packages=find_packages(), + include_package_data=True, + + entry_points={ + 'stevedore.example.formatter': [ + 'simple = stevedore.example.simple:Simple', + 'plain = stevedore.example.simple:Simple', + ], + }, + + zip_safe=False, +) diff --git a/libs/stevedore/example/simple.py b/libs/stevedore/example/simple.py new file mode 100644 index 00000000..1cad96af --- /dev/null +++ b/libs/stevedore/example/simple.py @@ -0,0 +1,20 @@ +from stevedore.example import base + + +class Simple(base.FormatterBase): + """A very basic formatter. + """ + + def format(self, data): + """Format the data and return unicode text. + + :param data: A dictionary with string keys and simple types as + values. + :type data: dict(str:?) + """ + for name, value in sorted(data.items()): + line = '{name} = {value}\n'.format( + name=name, + value=value, + ) + yield line diff --git a/libs/stevedore/example2/__init__.py b/libs/stevedore/example2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/stevedore/example2/fields.py b/libs/stevedore/example2/fields.py new file mode 100644 index 00000000..f5c8e194 --- /dev/null +++ b/libs/stevedore/example2/fields.py @@ -0,0 +1,36 @@ +import textwrap + +from stevedore.example import base + + +class FieldList(base.FormatterBase): + """Format values as a reStructuredText field list. + + For example:: + + : name1 : value + : name2 : value + : name3 : a long value + will be wrapped with + a hanging indent + """ + + def format(self, data): + """Format the data and return unicode text. + + :param data: A dictionary with string keys and simple types as + values. + :type data: dict(str:?) + """ + for name, value in sorted(data.items()): + full_text = ': {name} : {value}'.format( + name=name, + value=value, + ) + wrapped_text = textwrap.fill( + full_text, + initial_indent='', + subsequent_indent=' ', + width=self.max_width, + ) + yield wrapped_text + '\n' diff --git a/libs/stevedore/example2/setup.py b/libs/stevedore/example2/setup.py new file mode 100644 index 00000000..bd23838a --- /dev/null +++ b/libs/stevedore/example2/setup.py @@ -0,0 +1,42 @@ +from setuptools import setup, find_packages + +setup( + name='stevedore-examples2', + version='1.0', + + description='Demonstration package for stevedore', + + author='Doug Hellmann', + author_email='doug@doughellmann.com', + + url='http://git.openstack.org/cgit/openstack/stevedore', + + classifiers=['Development Status :: 3 - Alpha', + 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.5', + 'Intended Audience :: Developers', + 'Environment :: Console', + ], + + platforms=['Any'], + + scripts=[], + + provides=['stevedore.examples2', + ], + + packages=find_packages(), + include_package_data=True, + + entry_points={ + 'stevedore.example.formatter': [ + 'field = stevedore.example2.fields:FieldList', + ], + }, + + zip_safe=False, +) diff --git a/libs/stevedore/exception.py b/libs/stevedore/exception.py new file mode 100644 index 00000000..aa7f1451 --- /dev/null +++ b/libs/stevedore/exception.py @@ -0,0 +1,23 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +class NoUniqueMatch(RuntimeError): + """There was more than one extension, or none, that matched the query.""" + + +class NoMatches(NoUniqueMatch): + """There were no extensions with the driver name found.""" + + +class MultipleMatches(NoUniqueMatch): + """There were multiple matches for the given name.""" diff --git a/libs/stevedore/extension.py b/libs/stevedore/extension.py index 2dd22c74..f5c22928 100755 --- a/libs/stevedore/extension.py +++ b/libs/stevedore/extension.py @@ -1,10 +1,24 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + """ExtensionManager """ +import operator import pkg_resources import logging +from .exception import NoMatches LOG = logging.getLogger(__name__) @@ -139,20 +153,39 @@ class ExtensionManager(object): def _init_plugins(self, extensions): self.extensions = extensions - self._extensions_by_name = None + self._extensions_by_name_cache = None + + @property + def _extensions_by_name(self): + if self._extensions_by_name_cache is None: + d = {} + for e in self.extensions: + d[e.name] = e + self._extensions_by_name_cache = d + return self._extensions_by_name_cache ENTRY_POINT_CACHE = {} - def _find_entry_points(self, namespace): - if namespace not in self.ENTRY_POINT_CACHE: - eps = list(pkg_resources.iter_entry_points(namespace)) - self.ENTRY_POINT_CACHE[namespace] = eps - return self.ENTRY_POINT_CACHE[namespace] + def list_entry_points(self): + """Return the list of entry points for this namespace. + + The entry points are not actually loaded, their list is just read and + returned. + + """ + if self.namespace not in self.ENTRY_POINT_CACHE: + eps = list(pkg_resources.iter_entry_points(self.namespace)) + self.ENTRY_POINT_CACHE[self.namespace] = eps + return self.ENTRY_POINT_CACHE[self.namespace] + + def entry_points_names(self): + """Return the list of entry points names for this namespace.""" + return list(map(operator.attrgetter("name"), self.list_entry_points())) def _load_plugins(self, invoke_on_load, invoke_args, invoke_kwds, verify_requirements): extensions = [] - for ep in self._find_entry_points(self.namespace): + for ep in self.list_entry_points(): LOG.debug('found extension %r', ep) try: ext = self._load_one_plugin(ep, @@ -166,15 +199,30 @@ class ExtensionManager(object): except (KeyboardInterrupt, AssertionError): raise except Exception as err: - LOG.error('Could not load %r: %s', ep.name, err) - LOG.exception(err) if self._on_load_failure_callback is not None: self._on_load_failure_callback(self, ep, err) + else: + # Log the reason we couldn't import the module, + # usually without a traceback. The most common + # reason is an ImportError due to a missing + # dependency, and the error message should be + # enough to debug that. If debug logging is + # enabled for our logger, provide the full + # traceback. + LOG.error('Could not load %r: %s', ep.name, err, + exc_info=LOG.isEnabledFor(logging.DEBUG)) return extensions def _load_one_plugin(self, ep, invoke_on_load, invoke_args, invoke_kwds, verify_requirements): - plugin = ep.load(require=verify_requirements) + # NOTE(dhellmann): Using require=False is deprecated in + # setuptools 11.3. + if hasattr(ep, 'resolve') and hasattr(ep, 'require'): + if verify_requirements: + ep.require() + plugin = ep.resolve() + else: + plugin = ep.load(require=verify_requirements) if invoke_on_load: obj = plugin(*invoke_args, **invoke_kwds) else: @@ -210,7 +258,7 @@ class ExtensionManager(object): """ if not self.extensions: # FIXME: Use a more specific exception class here. - raise RuntimeError('No %s extensions found' % self.namespace) + raise NoMatches('No %s extensions found' % self.namespace) response = [] for e in self.extensions: self._invoke_one_plugin(response.append, func, e, args, kwds) @@ -252,6 +300,14 @@ class ExtensionManager(object): LOG.error('error calling %r: %s', e.name, err) LOG.exception(err) + def items(self): + """ + Return an iterator of tuples of the form (name, extension). + + This is analogous to the Mapping.items() method. + """ + return self._extensions_by_name.items() + def __iter__(self): """Produce iterator for the manager. @@ -267,9 +323,9 @@ class ExtensionManager(object): produces the :class:`Extension` instance with the specified name. """ - if self._extensions_by_name is None: - d = {} - for e in self.extensions: - d[e.name] = e - self._extensions_by_name = d return self._extensions_by_name[name] + + def __contains__(self, name): + """Return true if name is in list of enabled extensions. + """ + return any(extension.name == name for extension in self.extensions) diff --git a/libs/stevedore/hook.py b/libs/stevedore/hook.py index d2570db0..014b7c3c 100755 --- a/libs/stevedore/hook.py +++ b/libs/stevedore/hook.py @@ -1,3 +1,15 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + from .named import NamedExtensionManager @@ -27,12 +39,25 @@ class HookManager(NamedExtensionManager): :param verify_requirements: Use setuptools to enforce the dependencies of the plugin(s) being loaded. Defaults to False. :type verify_requirements: bool + :type on_missing_entrypoints_callback: function + :param verify_requirements: Use setuptools to enforce the + dependencies of the plugin(s) being loaded. Defaults to False. + :param warn_on_missing_entrypoint: Flag to control whether failing + to load a plugin is reported via a log mess. Only applies if + on_missing_entrypoints_callback is None. + :type warn_on_missing_entrypoint: bool + """ def __init__(self, namespace, name, invoke_on_load=False, invoke_args=(), invoke_kwds={}, on_load_failure_callback=None, - verify_requirements=False): + verify_requirements=False, + on_missing_entrypoints_callback=None, + # NOTE(dhellmann): This default is different from the + # base class because for hooks it is less likely to + # be an error to have no entry points present. + warn_on_missing_entrypoint=False): super(HookManager, self).__init__( namespace, [name], @@ -40,7 +65,9 @@ class HookManager(NamedExtensionManager): invoke_args=invoke_args, invoke_kwds=invoke_kwds, on_load_failure_callback=on_load_failure_callback, + on_missing_entrypoints_callback=on_missing_entrypoints_callback, verify_requirements=verify_requirements, + warn_on_missing_entrypoint=warn_on_missing_entrypoint, ) def _init_attributes(self, namespace, names, name_order=False, diff --git a/libs/stevedore/named.py b/libs/stevedore/named.py index 18fe235b..3b47dfd3 100755 --- a/libs/stevedore/named.py +++ b/libs/stevedore/named.py @@ -1,5 +1,21 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + from .extension import ExtensionManager +LOG = logging.getLogger(__name__) + class NamedExtensionManager(ExtensionManager): """Loads only the named extensions. @@ -34,9 +50,17 @@ class NamedExtensionManager(ExtensionManager): when this is called (when an entrypoint fails to load) are (manager, entrypoint, exception) :type on_load_failure_callback: function + :param on_missing_entrypoints_callback: Callback function that will be + called when one or more names cannot be found. The provided argument + will be a subset of the 'names' parameter. + :type on_missing_entrypoints_callback: function :param verify_requirements: Use setuptools to enforce the dependencies of the plugin(s) being loaded. Defaults to False. :type verify_requirements: bool + :param warn_on_missing_entrypoint: Flag to control whether failing + to load a plugin is reported via a log mess. Only applies if + on_missing_entrypoints_callback is None. + :type warn_on_missing_entrypoint: bool """ @@ -44,7 +68,9 @@ class NamedExtensionManager(ExtensionManager): invoke_on_load=False, invoke_args=(), invoke_kwds={}, name_order=False, propagate_map_exceptions=False, on_load_failure_callback=None, - verify_requirements=False): + on_missing_entrypoints_callback=None, + verify_requirements=False, + warn_on_missing_entrypoint=True): self._init_attributes( namespace, names, name_order=name_order, propagate_map_exceptions=propagate_map_exceptions, @@ -53,6 +79,13 @@ class NamedExtensionManager(ExtensionManager): invoke_args, invoke_kwds, verify_requirements) + self._missing_names = set(names) - set([e.name for e in extensions]) + if self._missing_names: + if on_missing_entrypoints_callback: + on_missing_entrypoints_callback(self._missing_names) + elif warn_on_missing_entrypoint: + LOG.warning('Could not load %s' % + ', '.join(self._missing_names)) self._init_plugins(extensions) @classmethod @@ -103,13 +136,15 @@ class NamedExtensionManager(ExtensionManager): on_load_failure_callback=on_load_failure_callback) self._names = names + self._missing_names = set() self._name_order = name_order def _init_plugins(self, extensions): super(NamedExtensionManager, self)._init_plugins(extensions) if self._name_order: - self.extensions = [self[n] for n in self._names] + self.extensions = [self[n] for n in self._names + if n not in self._missing_names] def _load_one_plugin(self, ep, invoke_on_load, invoke_args, invoke_kwds, verify_requirements): diff --git a/libs/stevedore/sphinxext.py b/libs/stevedore/sphinxext.py new file mode 100644 index 00000000..8ca88bbb --- /dev/null +++ b/libs/stevedore/sphinxext.py @@ -0,0 +1,115 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import unicode_literals + +import inspect + +from docutils import nodes +from docutils.parsers import rst +from docutils.parsers.rst import directives +from docutils.statemachine import ViewList +from sphinx.util import logging +from sphinx.util.nodes import nested_parse_with_titles + +from stevedore import extension + +LOG = logging.getLogger(__name__) + + +def _get_docstring(plugin): + return inspect.getdoc(plugin) or '' + + +def _simple_list(mgr): + for name in sorted(mgr.names()): + ext = mgr[name] + doc = _get_docstring(ext.plugin) or '\n' + summary = doc.splitlines()[0].strip() + yield('* %s -- %s' % (ext.name, summary), + ext.entry_point.module_name) + + +def _detailed_list(mgr, over='', under='-', titlecase=False): + for name in sorted(mgr.names()): + ext = mgr[name] + if over: + yield (over * len(ext.name), ext.entry_point.module_name) + if titlecase: + yield (ext.name.title(), ext.entry_point.module_name) + else: + yield (ext.name, ext.entry_point.module_name) + if under: + yield (under * len(ext.name), ext.entry_point.module_name) + yield ('\n', ext.entry_point.module_name) + doc = _get_docstring(ext.plugin) + if doc: + yield (doc, ext.entry_point.module_name) + else: + yield ('.. warning:: No documentation found in %s' + % ext.entry_point, + ext.entry_point.module_name) + yield ('\n', ext.entry_point.module_name) + + +class ListPluginsDirective(rst.Directive): + """Present a simple list of the plugins in a namespace.""" + + option_spec = { + 'class': directives.class_option, + 'detailed': directives.flag, + 'titlecase': directives.flag, + 'overline-style': directives.single_char_or_unicode, + 'underline-style': directives.single_char_or_unicode, + } + + has_content = True + + def run(self): + namespace = ' '.join(self.content).strip() + LOG.info('documenting plugins from %r' % namespace) + overline_style = self.options.get('overline-style', '') + underline_style = self.options.get('underline-style', '=') + + def report_load_failure(mgr, ep, err): + LOG.warning(u'Failed to load %s: %s' % (ep.module_name, err)) + + mgr = extension.ExtensionManager( + namespace, + on_load_failure_callback=report_load_failure, + ) + + result = ViewList() + + titlecase = 'titlecase' in self.options + + if 'detailed' in self.options: + data = _detailed_list( + mgr, over=overline_style, under=underline_style, + titlecase=titlecase) + else: + data = _simple_list(mgr) + for text, source in data: + for line in text.splitlines(): + result.append(line, source) + + # Parse what we have into a new section. + node = nodes.section() + node.document = self.state.document + nested_parse_with_titles(self.state, result, node) + + return node.children + + +def setup(app): + LOG.info('loading stevedore.sphinxext') + app.add_directive('list-plugins', ListPluginsDirective) diff --git a/libs/stevedore/tests/__init__.py b/libs/stevedore/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/stevedore/tests/extension_unimportable.py b/libs/stevedore/tests/extension_unimportable.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/stevedore/tests/manager.py b/libs/stevedore/tests/manager.py new file mode 100644 index 00000000..8c97a680 --- /dev/null +++ b/libs/stevedore/tests/manager.py @@ -0,0 +1,67 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""TestExtensionManager + +Extension manager used only for testing. +""" + +import warnings + +from stevedore import extension + + +class TestExtensionManager(extension.ExtensionManager): + """ExtensionManager that is explicitly initialized for tests. + + .. deprecated:: 0.13 + + Use the :func:`make_test_instance` class method of the class + being replaced by the test instance instead of using this class + directly. + + :param extensions: Pre-configured Extension instances to use + instead of loading them from entry points. + :type extensions: list of :class:`~stevedore.extension.Extension` + :param namespace: The namespace for the entry points. + :type namespace: str + :param invoke_on_load: Boolean controlling whether to invoke the + object returned by the entry point after the driver is loaded. + :type invoke_on_load: bool + :param invoke_args: Positional arguments to pass when invoking + the object returned by the entry point. Only used if invoke_on_load + is True. + :type invoke_args: tuple + :param invoke_kwds: Named arguments to pass when invoking + the object returned by the entry point. Only used if invoke_on_load + is True. + :type invoke_kwds: dict + + """ + + def __init__(self, extensions, + namespace='test', + invoke_on_load=False, + invoke_args=(), + invoke_kwds={}): + super(TestExtensionManager, self).__init__(namespace, + invoke_on_load, + invoke_args, + invoke_kwds, + ) + self.extensions = extensions + warnings.warn( + 'TestExtesionManager has been replaced by make_test_instance()', + DeprecationWarning) + + def _load_plugins(self, *args, **kwds): + return [] diff --git a/libs/stevedore/tests/test_callback.py b/libs/stevedore/tests/test_callback.py new file mode 100644 index 00000000..c513aa22 --- /dev/null +++ b/libs/stevedore/tests/test_callback.py @@ -0,0 +1,55 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Tests for failure loading callback +""" +from testtools.matchers import GreaterThan +import mock + +from stevedore import extension +from stevedore import named +from stevedore.tests import utils + + +class TestCallback(utils.TestCase): + def test_extension_failure_custom_callback(self): + errors = [] + + def failure_callback(manager, entrypoint, error): + errors.append((manager, entrypoint, error)) + + em = extension.ExtensionManager('stevedore.test.extension', + invoke_on_load=True, + on_load_failure_callback= + failure_callback) + extensions = list(em.extensions) + self.assertTrue(len(extensions), GreaterThan(0)) + self.assertEqual(len(errors), 2) + for manager, entrypoint, error in errors: + self.assertIs(manager, em) + self.assertIsInstance(error, (IOError, ImportError)) + + @mock.patch('stevedore.named.NamedExtensionManager._load_plugins') + def test_missing_entrypoints_callback(self, load_fn): + errors = set() + + def callback(names): + errors.update(names) + + load_fn.return_value = [ + extension.Extension('foo', None, None, None) + ] + named.NamedExtensionManager('stevedore.test.extension', + names=['foo', 'bar'], + invoke_on_load=True, + on_missing_entrypoints_callback=callback) + self.assertEqual(errors, {'bar'}) diff --git a/libs/stevedore/tests/test_dispatch.py b/libs/stevedore/tests/test_dispatch.py new file mode 100644 index 00000000..f1c305ab --- /dev/null +++ b/libs/stevedore/tests/test_dispatch.py @@ -0,0 +1,103 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from stevedore.tests import utils +from stevedore import dispatch + + +def check_dispatch(ep, *args, **kwds): + return ep.name == 't2' + + +class TestDispatch(utils.TestCase): + def check_dispatch(ep, *args, **kwds): + return ep.name == 't2' + + def test_dispatch(self): + + def invoke(ep, *args, **kwds): + return (ep.name, args, kwds) + + em = dispatch.DispatchExtensionManager('stevedore.test.extension', + lambda *args, **kwds: True, + invoke_on_load=True, + invoke_args=('a',), + invoke_kwds={'b': 'B'}, + ) + self.assertEqual(len(em.extensions), 2) + self.assertEqual(set(em.names()), set(['t1', 't2'])) + + results = em.map(check_dispatch, + invoke, + 'first', + named='named value', + ) + expected = [('t2', ('first',), {'named': 'named value'})] + self.assertEqual(results, expected) + + def test_dispatch_map_method(self): + em = dispatch.DispatchExtensionManager('stevedore.test.extension', + lambda *args, **kwds: True, + invoke_on_load=True, + invoke_args=('a',), + invoke_kwds={'b': 'B'}, + ) + + results = em.map_method(check_dispatch, 'get_args_and_data', 'first') + self.assertEqual(results, [(('a',), {'b': 'B'}, 'first')]) + + def test_name_dispatch(self): + + def invoke(ep, *args, **kwds): + return (ep.name, args, kwds) + + em = dispatch.NameDispatchExtensionManager('stevedore.test.extension', + lambda *args, **kwds: True, + invoke_on_load=True, + invoke_args=('a',), + invoke_kwds={'b': 'B'}, + ) + self.assertEqual(len(em.extensions), 2) + self.assertEqual(set(em.names()), set(['t1', 't2'])) + + results = em.map(['t2'], invoke, 'first', named='named value',) + expected = [('t2', ('first',), {'named': 'named value'})] + self.assertEqual(results, expected) + + def test_name_dispatch_ignore_missing(self): + + def invoke(ep, *args, **kwds): + return (ep.name, args, kwds) + + em = dispatch.NameDispatchExtensionManager( + 'stevedore.test.extension', + lambda *args, **kwds: True, + invoke_on_load=True, + invoke_args=('a',), + invoke_kwds={'b': 'B'}, + ) + + results = em.map(['t3', 't1'], invoke, 'first', named='named value',) + expected = [('t1', ('first',), {'named': 'named value'})] + self.assertEqual(results, expected) + + def test_name_dispatch_map_method(self): + em = dispatch.NameDispatchExtensionManager( + 'stevedore.test.extension', + lambda *args, **kwds: True, + invoke_on_load=True, + invoke_args=('a',), + invoke_kwds={'b': 'B'}, + ) + + results = em.map_method(['t3', 't1'], 'get_args_and_data', 'first') + self.assertEqual(results, [(('a',), {'b': 'B'}, 'first')]) diff --git a/libs/stevedore/tests/test_driver.py b/libs/stevedore/tests/test_driver.py new file mode 100644 index 00000000..c568a3a8 --- /dev/null +++ b/libs/stevedore/tests/test_driver.py @@ -0,0 +1,89 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Tests for stevedore.extension +""" + +import pkg_resources + +from stevedore import driver +from stevedore import exception +from stevedore import extension +from stevedore.tests import test_extension +from stevedore.tests import utils + + +class TestCallback(utils.TestCase): + def test_detect_plugins(self): + em = driver.DriverManager('stevedore.test.extension', 't1') + names = sorted(em.names()) + self.assertEqual(names, ['t1']) + + def test_call(self): + def invoke(ext, *args, **kwds): + return (ext.name, args, kwds) + em = driver.DriverManager('stevedore.test.extension', 't1') + result = em(invoke, 'a', b='C') + self.assertEqual(result, ('t1', ('a',), {'b': 'C'})) + + def test_driver_property_not_invoked_on_load(self): + em = driver.DriverManager('stevedore.test.extension', 't1', + invoke_on_load=False) + d = em.driver + self.assertIs(d, test_extension.FauxExtension) + + def test_driver_property_invoked_on_load(self): + em = driver.DriverManager('stevedore.test.extension', 't1', + invoke_on_load=True) + d = em.driver + self.assertIsInstance(d, test_extension.FauxExtension) + + def test_no_drivers(self): + try: + driver.DriverManager('stevedore.test.extension.none', 't1') + except exception.NoMatches as err: + self.assertIn("No 'stevedore.test.extension.none' driver found", + str(err)) + + def test_bad_driver(self): + try: + driver.DriverManager('stevedore.test.extension', 'e2') + except ImportError: + pass + else: + self.assertEqual(False, "No error raised") + + def test_multiple_drivers(self): + # The idea for this test was contributed by clayg: + # https://gist.github.com/clayg/6311348 + extensions = [ + extension.Extension( + 'backend', + pkg_resources.EntryPoint.parse('backend = pkg1:driver'), + 'pkg backend', + None, + ), + extension.Extension( + 'backend', + pkg_resources.EntryPoint.parse('backend = pkg2:driver'), + 'pkg backend', + None, + ), + ] + try: + dm = driver.DriverManager.make_test_instance(extensions[0]) + # Call the initialization code that verifies the extension + dm._init_plugins(extensions) + except exception.MultipleMatches as err: + self.assertIn("Multiple", str(err)) + else: + self.fail('Should have had an error') diff --git a/libs/stevedore/tests/test_enabled.py b/libs/stevedore/tests/test_enabled.py new file mode 100644 index 00000000..32cd1992 --- /dev/null +++ b/libs/stevedore/tests/test_enabled.py @@ -0,0 +1,42 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from stevedore import enabled +from stevedore.tests import utils + + +class TestEnabled(utils.TestCase): + def test_enabled(self): + def check_enabled(ep): + return ep.name == 't2' + em = enabled.EnabledExtensionManager( + 'stevedore.test.extension', + check_enabled, + invoke_on_load=True, + invoke_args=('a',), + invoke_kwds={'b': 'B'}, + ) + self.assertEqual(len(em.extensions), 1) + self.assertEqual(em.names(), ['t2']) + + def test_enabled_after_load(self): + def check_enabled(ext): + return ext.obj and ext.name == 't2' + em = enabled.EnabledExtensionManager( + 'stevedore.test.extension', + check_enabled, + invoke_on_load=True, + invoke_args=('a',), + invoke_kwds={'b': 'B'}, + ) + self.assertEqual(len(em.extensions), 1) + self.assertEqual(em.names(), ['t2']) diff --git a/libs/stevedore/tests/test_example_fields.py b/libs/stevedore/tests/test_example_fields.py new file mode 100644 index 00000000..757917c9 --- /dev/null +++ b/libs/stevedore/tests/test_example_fields.py @@ -0,0 +1,41 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Tests for stevedore.example2.fields +""" + +from stevedore.example2 import fields +from stevedore.tests import utils + + +class TestExampleFields(utils.TestCase): + def test_simple_items(self): + f = fields.FieldList(100) + text = ''.join(f.format({'a': 'A', 'b': 'B'})) + expected = '\n'.join([ + ': a : A', + ': b : B', + '', + ]) + self.assertEqual(text, expected) + + def test_long_item(self): + f = fields.FieldList(25) + text = ''.join(f.format({'name': + 'a value longer than the allowed width'})) + expected = '\n'.join([ + ': name : a value longer', + ' than the allowed', + ' width', + '', + ]) + self.assertEqual(text, expected) diff --git a/libs/stevedore/tests/test_example_simple.py b/libs/stevedore/tests/test_example_simple.py new file mode 100644 index 00000000..382ed899 --- /dev/null +++ b/libs/stevedore/tests/test_example_simple.py @@ -0,0 +1,29 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Tests for stevedore.example.simple +""" + +from stevedore.example import simple +from stevedore.tests import utils + + +class TestExampleSimple(utils.TestCase): + def test_simple_items(self): + f = simple.Simple(100) + text = ''.join(f.format({'a': 'A', 'b': 'B'})) + expected = '\n'.join([ + 'a = A', + 'b = B', + '', + ]) + self.assertEqual(text, expected) diff --git a/libs/stevedore/tests/test_extension.py b/libs/stevedore/tests/test_extension.py new file mode 100644 index 00000000..17f270fc --- /dev/null +++ b/libs/stevedore/tests/test_extension.py @@ -0,0 +1,244 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Tests for stevedore.extension +""" + +import operator + +import mock + +from stevedore import exception +from stevedore import extension +from stevedore.tests import utils + + +ALL_NAMES = ['e1', 't1', 't2'] +WORKING_NAMES = ['t1', 't2'] + + +class FauxExtension(object): + def __init__(self, *args, **kwds): + self.args = args + self.kwds = kwds + + def get_args_and_data(self, data): + return self.args, self.kwds, data + + +class BrokenExtension(object): + def __init__(self, *args, **kwds): + raise IOError("Did not create") + + +class TestCallback(utils.TestCase): + def test_detect_plugins(self): + em = extension.ExtensionManager('stevedore.test.extension') + names = sorted(em.names()) + self.assertEqual(names, ALL_NAMES) + + def test_get_by_name(self): + em = extension.ExtensionManager('stevedore.test.extension') + e = em['t1'] + self.assertEqual(e.name, 't1') + + def test_list_entry_points(self): + em = extension.ExtensionManager('stevedore.test.extension') + n = em.list_entry_points() + self.assertEqual(set(['e1', 'e2', 't1', 't2']), + set(map(operator.attrgetter("name"), n))) + self.assertEqual(4, len(n)) + + def test_list_entry_points_names(self): + em = extension.ExtensionManager('stevedore.test.extension') + names = em.entry_points_names() + self.assertEqual(set(['e1', 'e2', 't1', 't2']), set(names)) + self.assertEqual(4, len(names)) + + def test_contains_by_name(self): + em = extension.ExtensionManager('stevedore.test.extension') + self.assertEqual('t1' in em, True) + + def test_get_by_name_missing(self): + em = extension.ExtensionManager('stevedore.test.extension') + try: + em['t3'] + except KeyError: + pass + else: + assert False, 'Failed to raise KeyError' + + def test_load_multiple_times_entry_points(self): + # We expect to get the same EntryPoint object because we save them + # in the cache. + em1 = extension.ExtensionManager('stevedore.test.extension') + eps1 = [ext.entry_point for ext in em1] + em2 = extension.ExtensionManager('stevedore.test.extension') + eps2 = [ext.entry_point for ext in em2] + self.assertIs(eps1[0], eps2[0]) + + def test_load_multiple_times_plugins(self): + # We expect to get the same plugin object (module or class) + # because the underlying import machinery will cache the values. + em1 = extension.ExtensionManager('stevedore.test.extension') + plugins1 = [ext.plugin for ext in em1] + em2 = extension.ExtensionManager('stevedore.test.extension') + plugins2 = [ext.plugin for ext in em2] + self.assertIs(plugins1[0], plugins2[0]) + + def test_use_cache(self): + # If we insert something into the cache of entry points, + # the manager should not have to call into pkg_resources + # to find the plugins. + cache = extension.ExtensionManager.ENTRY_POINT_CACHE + cache['stevedore.test.faux'] = [] + with mock.patch('pkg_resources.iter_entry_points', + side_effect= + AssertionError('called iter_entry_points')): + em = extension.ExtensionManager('stevedore.test.faux') + names = em.names() + self.assertEqual(names, []) + + def test_iterable(self): + em = extension.ExtensionManager('stevedore.test.extension') + names = sorted(e.name for e in em) + self.assertEqual(names, ALL_NAMES) + + def test_invoke_on_load(self): + em = extension.ExtensionManager('stevedore.test.extension', + invoke_on_load=True, + invoke_args=('a',), + invoke_kwds={'b': 'B'}, + ) + self.assertEqual(len(em.extensions), 2) + for e in em.extensions: + self.assertEqual(e.obj.args, ('a',)) + self.assertEqual(e.obj.kwds, {'b': 'B'}) + + def test_map_return_values(self): + def mapped(ext, *args, **kwds): + return ext.name + + em = extension.ExtensionManager('stevedore.test.extension', + invoke_on_load=True, + ) + results = em.map(mapped) + self.assertEqual(sorted(results), WORKING_NAMES) + + def test_map_arguments(self): + objs = [] + + def mapped(ext, *args, **kwds): + objs.append((ext, args, kwds)) + + em = extension.ExtensionManager('stevedore.test.extension', + invoke_on_load=True, + ) + em.map(mapped, 1, 2, a='A', b='B') + self.assertEqual(len(objs), 2) + names = sorted([o[0].name for o in objs]) + self.assertEqual(names, WORKING_NAMES) + for o in objs: + self.assertEqual(o[1], (1, 2)) + self.assertEqual(o[2], {'a': 'A', 'b': 'B'}) + + def test_map_eats_errors(self): + def mapped(ext, *args, **kwds): + raise RuntimeError('hard coded error') + + em = extension.ExtensionManager('stevedore.test.extension', + invoke_on_load=True, + ) + results = em.map(mapped, 1, 2, a='A', b='B') + self.assertEqual(results, []) + + def test_map_propagate_exceptions(self): + def mapped(ext, *args, **kwds): + raise RuntimeError('hard coded error') + + em = extension.ExtensionManager('stevedore.test.extension', + invoke_on_load=True, + propagate_map_exceptions=True + ) + + try: + em.map(mapped, 1, 2, a='A', b='B') + assert False + except RuntimeError: + pass + + def test_map_errors_when_no_plugins(self): + expected_str = 'No stevedore.test.extension.none extensions found' + + def mapped(ext, *args, **kwds): + pass + + em = extension.ExtensionManager('stevedore.test.extension.none', + invoke_on_load=True, + ) + try: + em.map(mapped, 1, 2, a='A', b='B') + except exception.NoMatches as err: + self.assertEqual(expected_str, str(err)) + + def test_map_method(self): + em = extension.ExtensionManager('stevedore.test.extension', + invoke_on_load=True, + ) + + result = em.map_method('get_args_and_data', 42) + self.assertEqual(set(r[2] for r in result), set([42])) + + def test_items(self): + em = extension.ExtensionManager('stevedore.test.extension') + expected_output = set([(name, em[name]) for name in ALL_NAMES]) + self.assertEqual(expected_output, set(em.items())) + + +class TestLoadRequirementsNewSetuptools(utils.TestCase): + # setuptools 11.3 and later + + def setUp(self): + super(TestLoadRequirementsNewSetuptools, self).setUp() + self.mock_ep = mock.Mock(spec=['require', 'resolve', 'load', 'name']) + self.em = extension.ExtensionManager.make_test_instance([]) + + def test_verify_requirements(self): + self.em._load_one_plugin(self.mock_ep, False, (), {}, + verify_requirements=True) + self.mock_ep.require.assert_called_once_with() + self.mock_ep.resolve.assert_called_once_with() + + def test_no_verify_requirements(self): + self.em._load_one_plugin(self.mock_ep, False, (), {}, + verify_requirements=False) + self.assertEqual(0, self.mock_ep.require.call_count) + self.mock_ep.resolve.assert_called_once_with() + + +class TestLoadRequirementsOldSetuptools(utils.TestCase): + # Before setuptools 11.3 + + def setUp(self): + super(TestLoadRequirementsOldSetuptools, self).setUp() + self.mock_ep = mock.Mock(spec=['load', 'name']) + self.em = extension.ExtensionManager.make_test_instance([]) + + def test_verify_requirements(self): + self.em._load_one_plugin(self.mock_ep, False, (), {}, + verify_requirements=True) + self.mock_ep.load.assert_called_once_with(require=True) + + def test_no_verify_requirements(self): + self.em._load_one_plugin(self.mock_ep, False, (), {}, + verify_requirements=False) + self.mock_ep.load.assert_called_once_with(require=False) diff --git a/libs/stevedore/tests/test_hook.py b/libs/stevedore/tests/test_hook.py new file mode 100644 index 00000000..5741bb9f --- /dev/null +++ b/libs/stevedore/tests/test_hook.py @@ -0,0 +1,55 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from stevedore import hook +from stevedore.tests import utils + + +class TestHook(utils.TestCase): + def test_hook(self): + em = hook.HookManager( + 'stevedore.test.extension', + 't1', + invoke_on_load=True, + invoke_args=('a',), + invoke_kwds={'b': 'B'}, + ) + self.assertEqual(len(em.extensions), 1) + self.assertEqual(em.names(), ['t1']) + + def test_get_by_name(self): + em = hook.HookManager( + 'stevedore.test.extension', + 't1', + invoke_on_load=True, + invoke_args=('a',), + invoke_kwds={'b': 'B'}, + ) + e_list = em['t1'] + self.assertEqual(len(e_list), 1) + e = e_list[0] + self.assertEqual(e.name, 't1') + + def test_get_by_name_missing(self): + em = hook.HookManager( + 'stevedore.test.extension', + 't1', + invoke_on_load=True, + invoke_args=('a',), + invoke_kwds={'b': 'B'}, + ) + try: + em['t2'] + except KeyError: + pass + else: + assert False, 'Failed to raise KeyError' diff --git a/libs/stevedore/tests/test_named.py b/libs/stevedore/tests/test_named.py new file mode 100644 index 00000000..757d0aab --- /dev/null +++ b/libs/stevedore/tests/test_named.py @@ -0,0 +1,93 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from stevedore import named +from stevedore.tests import utils + +import mock + + +class TestNamed(utils.TestCase): + def test_named(self): + em = named.NamedExtensionManager( + 'stevedore.test.extension', + names=['t1'], + invoke_on_load=True, + invoke_args=('a',), + invoke_kwds={'b': 'B'}, + ) + actual = em.names() + self.assertEqual(actual, ['t1']) + + def test_enabled_before_load(self): + # Set up the constructor for the FauxExtension to cause an + # AssertionError so the test fails if the class is instantiated, + # which should only happen if it is loaded before the name of the + # extension is compared against the names that should be loaded by + # the manager. + init_name = 'stevedore.tests.test_extension.FauxExtension.__init__' + with mock.patch(init_name) as m: + m.side_effect = AssertionError + em = named.NamedExtensionManager( + 'stevedore.test.extension', + # Look for an extension that does not exist so the + # __init__ we mocked should never be invoked. + names=['no-such-extension'], + invoke_on_load=True, + invoke_args=('a',), + invoke_kwds={'b': 'B'}, + ) + actual = em.names() + self.assertEqual(actual, []) + + def test_extensions_listed_in_name_order(self): + # Since we don't know the "natural" order of the extensions, run + # the test both ways: if the sorting is broken, one of them will + # fail + em = named.NamedExtensionManager( + 'stevedore.test.extension', + names=['t1', 't2'], + name_order=True + ) + actual = em.names() + self.assertEqual(actual, ['t1', 't2']) + + em = named.NamedExtensionManager( + 'stevedore.test.extension', + names=['t2', 't1'], + name_order=True + ) + actual = em.names() + self.assertEqual(actual, ['t2', 't1']) + + def test_load_fail_ignored_when_sorted(self): + em = named.NamedExtensionManager( + 'stevedore.test.extension', + names=['e1', 't2', 'e2', 't1'], + name_order=True, + invoke_on_load=True, + invoke_args=('a',), + invoke_kwds={'b': 'B'}, + ) + actual = em.names() + self.assertEqual(['t2', 't1'], actual) + + em = named.NamedExtensionManager( + 'stevedore.test.extension', + names=['e1', 't1'], + name_order=False, + invoke_on_load=True, + invoke_args=('a',), + invoke_kwds={'b': 'B'}, + ) + actual = em.names() + self.assertEqual(['t1'], actual) diff --git a/libs/stevedore/tests/test_sphinxext.py b/libs/stevedore/tests/test_sphinxext.py new file mode 100644 index 00000000..60b47944 --- /dev/null +++ b/libs/stevedore/tests/test_sphinxext.py @@ -0,0 +1,120 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""Tests for the sphinx extension +""" + +from __future__ import unicode_literals + +from stevedore import extension +from stevedore import sphinxext +from stevedore.tests import utils + +import mock +import pkg_resources + + +def _make_ext(name, docstring): + def inner(): + pass + + inner.__doc__ = docstring + m1 = mock.Mock(spec=pkg_resources.EntryPoint) + m1.module_name = '%s_module' % name + s = mock.Mock(return_value='ENTRY_POINT(%s)' % name) + m1.__str__ = s + return extension.Extension(name, m1, inner, None) + + +class TestSphinxExt(utils.TestCase): + + def setUp(self): + super(TestSphinxExt, self).setUp() + self.exts = [ + _make_ext('test1', 'One-line docstring'), + _make_ext('test2', 'Multi-line docstring\n\nAnother para'), + ] + self.em = extension.ExtensionManager.make_test_instance(self.exts) + + def test_simple_list(self): + results = list(sphinxext._simple_list(self.em)) + self.assertEqual( + [ + ('* test1 -- One-line docstring', 'test1_module'), + ('* test2 -- Multi-line docstring', 'test2_module'), + ], + results, + ) + + def test_simple_list_no_docstring(self): + ext = [_make_ext('nodoc', None)] + em = extension.ExtensionManager.make_test_instance(ext) + results = list(sphinxext._simple_list(em)) + self.assertEqual( + [ + ('* nodoc -- ', 'nodoc_module'), + ], + results, + ) + + def test_detailed_list(self): + results = list(sphinxext._detailed_list(self.em)) + self.assertEqual( + [ + ('test1', 'test1_module'), + ('-----', 'test1_module'), + ('\n', 'test1_module'), + ('One-line docstring', 'test1_module'), + ('\n', 'test1_module'), + ('test2', 'test2_module'), + ('-----', 'test2_module'), + ('\n', 'test2_module'), + ('Multi-line docstring\n\nAnother para', 'test2_module'), + ('\n', 'test2_module'), + ], + results, + ) + + def test_detailed_list_format(self): + results = list(sphinxext._detailed_list(self.em, over='+', under='+')) + self.assertEqual( + [ + ('+++++', 'test1_module'), + ('test1', 'test1_module'), + ('+++++', 'test1_module'), + ('\n', 'test1_module'), + ('One-line docstring', 'test1_module'), + ('\n', 'test1_module'), + ('+++++', 'test2_module'), + ('test2', 'test2_module'), + ('+++++', 'test2_module'), + ('\n', 'test2_module'), + ('Multi-line docstring\n\nAnother para', 'test2_module'), + ('\n', 'test2_module'), + ], + results, + ) + + def test_detailed_list_no_docstring(self): + ext = [_make_ext('nodoc', None)] + em = extension.ExtensionManager.make_test_instance(ext) + results = list(sphinxext._detailed_list(em)) + self.assertEqual( + [ + ('nodoc', 'nodoc_module'), + ('-----', 'nodoc_module'), + ('\n', 'nodoc_module'), + ('.. warning:: No documentation found in ENTRY_POINT(nodoc)', + 'nodoc_module'), + ('\n', 'nodoc_module'), + ], + results, + ) diff --git a/libs/stevedore/tests/test_test_manager.py b/libs/stevedore/tests/test_test_manager.py new file mode 100644 index 00000000..df056cfe --- /dev/null +++ b/libs/stevedore/tests/test_test_manager.py @@ -0,0 +1,216 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from mock import Mock, sentinel +from stevedore import (ExtensionManager, NamedExtensionManager, HookManager, + DriverManager, EnabledExtensionManager) +from stevedore.dispatch import (DispatchExtensionManager, + NameDispatchExtensionManager) +from stevedore.extension import Extension +from stevedore.tests import utils + + +test_extension = Extension('test_extension', None, None, None) +test_extension2 = Extension('another_one', None, None, None) + +mock_entry_point = Mock(module_name='test.extension', attrs=['obj']) +a_driver = Extension('test_driver', mock_entry_point, sentinel.driver_plugin, + sentinel.driver_obj) + + +# base ExtensionManager +class TestTestManager(utils.TestCase): + def test_instance_should_use_supplied_extensions(self): + extensions = [test_extension, test_extension2] + em = ExtensionManager.make_test_instance(extensions) + self.assertEqual(extensions, em.extensions) + + def test_instance_should_have_default_namespace(self): + em = ExtensionManager.make_test_instance([]) + self.assertEqual(em.namespace, 'TESTING') + + def test_instance_should_use_supplied_namespace(self): + namespace = 'testing.1.2.3' + em = ExtensionManager.make_test_instance([], namespace=namespace) + self.assertEqual(namespace, em.namespace) + + def test_extension_name_should_be_listed(self): + em = ExtensionManager.make_test_instance([test_extension]) + self.assertIn(test_extension.name, em.names()) + + def test_iterator_should_yield_extension(self): + em = ExtensionManager.make_test_instance([test_extension]) + self.assertEqual(test_extension, next(iter(em))) + + def test_manager_should_allow_name_access(self): + em = ExtensionManager.make_test_instance([test_extension]) + self.assertEqual(test_extension, em[test_extension.name]) + + def test_manager_should_call(self): + em = ExtensionManager.make_test_instance([test_extension]) + func = Mock() + em.map(func) + func.assert_called_once_with(test_extension) + + def test_manager_should_call_all(self): + em = ExtensionManager.make_test_instance([test_extension2, + test_extension]) + func = Mock() + em.map(func) + func.assert_any_call(test_extension2) + func.assert_any_call(test_extension) + + def test_manager_return_values(self): + def mapped(ext, *args, **kwds): + return ext.name + + em = ExtensionManager.make_test_instance([test_extension2, + test_extension]) + results = em.map(mapped) + self.assertEqual(sorted(results), ['another_one', 'test_extension']) + + def test_manager_should_eat_exceptions(self): + em = ExtensionManager.make_test_instance([test_extension]) + + func = Mock(side_effect=RuntimeError('hard coded error')) + + results = em.map(func, 1, 2, a='A', b='B') + self.assertEqual(results, []) + + def test_manager_should_propagate_exceptions(self): + em = ExtensionManager.make_test_instance([test_extension], + propagate_map_exceptions=True) + self.skipTest('Skipping temporarily') + func = Mock(side_effect=RuntimeError('hard coded error')) + em.map(func, 1, 2, a='A', b='B') + + # NamedExtensionManager + def test_named_manager_should_use_supplied_extensions(self): + extensions = [test_extension, test_extension2] + em = NamedExtensionManager.make_test_instance(extensions) + self.assertEqual(extensions, em.extensions) + + def test_named_manager_should_have_default_namespace(self): + em = NamedExtensionManager.make_test_instance([]) + self.assertEqual(em.namespace, 'TESTING') + + def test_named_manager_should_use_supplied_namespace(self): + namespace = 'testing.1.2.3' + em = NamedExtensionManager.make_test_instance([], namespace=namespace) + self.assertEqual(namespace, em.namespace) + + def test_named_manager_should_populate_names(self): + extensions = [test_extension, test_extension2] + em = NamedExtensionManager.make_test_instance(extensions) + self.assertEqual(em.names(), ['test_extension', 'another_one']) + + # HookManager + def test_hook_manager_should_use_supplied_extensions(self): + extensions = [test_extension, test_extension2] + em = HookManager.make_test_instance(extensions) + self.assertEqual(extensions, em.extensions) + + def test_hook_manager_should_be_first_extension_name(self): + extensions = [test_extension, test_extension2] + em = HookManager.make_test_instance(extensions) + # This will raise KeyError if the names don't match + assert(em[test_extension.name]) + + def test_hook_manager_should_have_default_namespace(self): + em = HookManager.make_test_instance([test_extension]) + self.assertEqual(em.namespace, 'TESTING') + + def test_hook_manager_should_use_supplied_namespace(self): + namespace = 'testing.1.2.3' + em = HookManager.make_test_instance([test_extension], + namespace=namespace) + self.assertEqual(namespace, em.namespace) + + def test_hook_manager_should_return_named_extensions(self): + hook1 = Extension('captain', None, None, None) + hook2 = Extension('captain', None, None, None) + em = HookManager.make_test_instance([hook1, hook2]) + self.assertEqual([hook1, hook2], em['captain']) + + # DriverManager + def test_driver_manager_should_use_supplied_extension(self): + em = DriverManager.make_test_instance(a_driver) + self.assertEqual([a_driver], em.extensions) + + def test_driver_manager_should_have_default_namespace(self): + em = DriverManager.make_test_instance(a_driver) + self.assertEqual(em.namespace, 'TESTING') + + def test_driver_manager_should_use_supplied_namespace(self): + namespace = 'testing.1.2.3' + em = DriverManager.make_test_instance(a_driver, namespace=namespace) + self.assertEqual(namespace, em.namespace) + + def test_instance_should_use_driver_name(self): + em = DriverManager.make_test_instance(a_driver) + self.assertEqual(['test_driver'], em.names()) + + def test_instance_call(self): + def invoke(ext, *args, **kwds): + return ext.name, args, kwds + + em = DriverManager.make_test_instance(a_driver) + result = em(invoke, 'a', b='C') + self.assertEqual(result, ('test_driver', ('a',), {'b': 'C'})) + + def test_instance_driver_property(self): + em = DriverManager.make_test_instance(a_driver) + self.assertEqual(sentinel.driver_obj, em.driver) + + # EnabledExtensionManager + def test_enabled_instance_should_use_supplied_extensions(self): + extensions = [test_extension, test_extension2] + em = EnabledExtensionManager.make_test_instance(extensions) + self.assertEqual(extensions, em.extensions) + + # DispatchExtensionManager + def test_dispatch_instance_should_use_supplied_extensions(self): + extensions = [test_extension, test_extension2] + em = DispatchExtensionManager.make_test_instance(extensions) + self.assertEqual(extensions, em.extensions) + + def test_dispatch_map_should_invoke_filter_for_extensions(self): + em = DispatchExtensionManager.make_test_instance([test_extension, + test_extension2]) + filter_func = Mock(return_value=False) + args = ('A',) + kw = {'big': 'Cheese'} + em.map(filter_func, None, *args, **kw) + filter_func.assert_any_call(test_extension, *args, **kw) + filter_func.assert_any_call(test_extension2, *args, **kw) + + # NameDispatchExtensionManager + def test_name_dispatch_instance_should_use_supplied_extensions(self): + extensions = [test_extension, test_extension2] + em = NameDispatchExtensionManager.make_test_instance(extensions) + + self.assertEqual(extensions, em.extensions) + + def test_name_dispatch_instance_should_build_extension_name_map(self): + extensions = [test_extension, test_extension2] + em = NameDispatchExtensionManager.make_test_instance(extensions) + self.assertEqual(test_extension, em.by_name[test_extension.name]) + self.assertEqual(test_extension2, em.by_name[test_extension2.name]) + + def test_named_dispatch_map_should_invoke_filter_for_extensions(self): + em = NameDispatchExtensionManager.make_test_instance([test_extension, + test_extension2]) + func = Mock() + args = ('A',) + kw = {'BIGGER': 'Cheese'} + em.map(['test_extension'], func, *args, **kw) + func.assert_called_once_with(test_extension, *args, **kw) diff --git a/libs/stevedore/tests/utils.py b/libs/stevedore/tests/utils.py new file mode 100644 index 00000000..f452959c --- /dev/null +++ b/libs/stevedore/tests/utils.py @@ -0,0 +1,17 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import unittest + + +class TestCase(unittest.TestCase): + pass diff --git a/libs/subliminal/subtitles/__init__.py b/libs/subliminal/subtitles/__init__.py new file mode 100644 index 00000000..2bddfe0f --- /dev/null +++ b/libs/subliminal/subtitles/__init__.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +from datetime import time + + +class Component(object): + """Base class for cue text. + + :param list components: sub-components of this one. + + """ + tag_name = 'Component' + + def __init__(self, components=None): + if components is None: + self.components = [] + elif isinstance(components, list): + self.components = components + else: + self.components = [components] + + def __iter__(self): + return iter(self.components) + + def __len__(self): + return len(self.components) + + def __str__(self): + return ''.join(str(c) for c in self.components) + + def __repr__(self): + return '<{name}>{components}'.format(name=self.tag_name, + components=''.join(repr(c) for c in self.components)) + + +class Bold(Component): + """Bold :class:`Component`.""" + tag_name = 'b' + + +class Italic(Component): + """Italic :class:`Component`.""" + tag_name = 'i' + + +class Underline(Component): + """Underline :class:`Component`.""" + tag_name = 'u' + + +class Strikethrough(Component): + """Strikethrough :class:`Component`.""" + tag_name = 's' + + +class Font(Component): + """Font :class:`Component`.""" + tag_name = 'font' + + def __init__(self, color, *args, **kwargs): + super(Font, self).__init__(*args, **kwargs) + self.color = color + + def __repr__(self): + return '<{name} "{color}">{components}'.format(name=self.tag_name, color=self.color, + components=''.join(repr(c) for c in self.components)) + + +class Cue(object): + """A single subtitle cue with timings and components. + + :param datetime.time start_time: start time. + :param datetime.time end_time: end time. + :param list components: cue components. + + """ + def __init__(self, start_time, end_time, components): + self.start_time = start_time + self.end_time = end_time + self.components = components + + def __repr__(self): + return '{end_time}] "{text}">'.format(start_time=self.start_time, end_time=self.end_time, + text=''.join(repr(c) for c in self.components)) + + +if __name__ == '__main__': + cue = Cue(time(), time(1), [Bold('Hello')]) + print repr(cue) diff --git a/libs/subliminal/subtitles/subrip.py b/libs/subliminal/subtitles/subrip.py new file mode 100644 index 00000000..47e5b477 --- /dev/null +++ b/libs/subliminal/subtitles/subrip.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +import re +from datetime import time + +from subliminal.subtitles import Cue + +index_re = re.compile(r'(?P\d+)') +timing_re = re.compile(r'(?P\d{2}):(?P\d{2}):(?P\d{2}),(?P\d{3})') + + +class SubripReadError(Exception): + pass + + +class SubripReadIndexError(SubripReadError): + pass + + +class SubripReader(object): + INDEX = 1 + TIMINGS = 2 + TEXT = 3 + + def __init__(self): + self.state = self.INDEX + + def read(self, content): + pass + + def read_line(self, line): + if self.state == self.INDEX: + if index_re.match(line): + raise SubripReadIndexError + + +def read_cue(stream): + """Attempt to parse a complete Cue from the stream""" + # skip blank lines + line = '' + while not line: + line = stream.readline() + + # parse index + if not index_re.match(line): + raise SubripReadIndexError + + # parse timings + line = stream.readline() + if '-->' not in line: + raise SubripReadError + timings = line.split('-->') + if not len(timings): + raise SubripReadError + + # parse start time + match = timing_re.match(timings[0].strip()) + if not match: + raise SubripReadError + start_time = time(**match.groupdict()) + + # parse end time + match = timing_re.match(timings[0].strip()) + if not match: + raise SubripReadError + end_time = time(**match.groupdict()) + + + + +class SubripSubtitle(object): + def __init__(self): + self.cues = [] + + +if __name__ == '__main__': + print read_cue('toto') + i = 0 + for x in read_cue('toto'): + print x + if i > 10: + break + i += 1