def test_ListOf_error(): config = ConfigManager.from_dict({'bools': 't,f,badbool'}) if six.PY3: with pytest.raises(InvalidValueError) as exc_info: config('bools', parser=ListOf(bool)) else: with pytest.raises(ValueError) as exc_info: config('bools', parser=ListOf(bool)) assert ( str(exc_info.value) == 'ValueError: "badbool" is not a valid bool value\n' 'namespace=None key=bools requires a value parseable by <ListOf(bool)>' )
class ComponentOptionParser(RequiredConfigMixin): required_config = ConfigOptions() required_config.add_option('user_builtin', parser=int) required_config.add_option('user_parse_class', parser=parse_class) required_config.add_option('user_listof', parser=ListOf(str)) required_config.add_option('user_class_method', parser=Foo.parse_foo_class) required_config.add_option('user_instance_method', parser=Foo().parse_foo_instance)
def test_ListOf_error(): config = ConfigManager.from_dict({"bools": "t,f,badbool"}) with pytest.raises(InvalidValueError) as exc_info: config("bools", parser=ListOf(bool)) assert ( str(exc_info.value) == 'ValueError: "badbool" is not a valid bool value\n' "namespace=None key=bools requires a value parseable by <ListOf(bool)>" )
def get_deploy_branches(repo_full_name): """ Get repo specific branch list to deploy if configured. :return: list of branch names """ env_var = slugify(repo_full_name).replace('-', '_').upper() env_var = f'{env_var}_BRANCHES' branches = config(env_var, parser=ListOf(str), default='') if not branches: branches = DEFAULT_DEPLOY_BRANCHES return branches
class Core(Config): DEBUG = Param('debug', default='false', doc='turn debug on', parser=bool) ADDITIONAL_EXTENSIONS = Param( 'extensions', default='', doc='comma-separated list of additional ebonite extensions to load', parser=ListOf(str), raise_error=False) AUTO_IMPORT_EXTENSIONS = Param( 'auto_import_extensions', default='true', doc= 'Set to true to automatically load available extensions on ebonite import', parser=bool) RUNTIME = Param('runtime', default='false', doc='is this instance a runtime', parser=bool)
class AppConfig(RequiredConfigMixin): """Application-level config To pull out a config item, you can do this:: config = ConfigManager([ConfigOSEnv()]) app_config = AppConfig(config) debug = app_config('debug') To create a component with configuration, you can do this:: class SomeComponent(RequiredConfigMixin): required_config = ConfigOptions() def __init__(self, config): self.config = config.with_options(self) some_component = SomeComponent(app_config.config) To pass application-level configuration to components, you should do it through arguments like this:: class SomeComponent(RequiredConfigMixin): required_config = ConfigOptions() def __init__(self, config, debug): self.config = config.with_options(self) self.debug = debug some_component = SomeComponent(app_config.config_manager, debug) """ required_config = ConfigOptions() required_config.add_option( 'basedir', default=str(Path(__file__).parent.parent), doc='The root directory for this application to find and store things.' ) required_config.add_option( 'logging_level', default='DEBUG', doc='The logging level to use. DEBUG, INFO, WARNING, ERROR or CRITICAL' ) required_config.add_option( 'metrics_class', default='jansky.metrics.LoggingMetrics', doc= ('Comma-separated list of metrics backends to use. Possible options: ' '"jansky.metrics.LoggingMetrics" and "jansky.metrics.DatadogMetrics"', ), parser=ListOf(parse_class)) required_config.add_option( 'secret_sentry_dsn', default='', doc= ('Sentry DSN to use. See https://docs.sentry.io/quickstart/#configure-the-dsn ' 'for details. If this is not set an unhandled exception logging middleware ' 'will be used instead.')) def __init__(self, config): self.config_manager = config self.config = config.with_options(self) def __call__(self, key): return self.config(key)
def get_download_url(self, channel, version, platform, locale, force_direct=False, force_full_installer=False, force_funnelcake=False, funnelcake_id=None, locale_in_transition=False): """ Get direct download url for the product. :param channel: one of self.version_map.keys(). :param version: a firefox version. one of self.latest_version. :param platform: OS. one of self.platform_labels.keys(). :param locale: e.g. pt-BR. one exception is ja-JP-mac. :param force_direct: Force the download URL to be direct. always True for non-release URLs. :param force_full_installer: Force the installer download to not be the stub installer (for aurora). :param force_funnelcake: Force the download version for en-US Windows to be 'latest', which bouncer will translate to the funnelcake build. :param funnelcake_id: ID for the the funnelcake build. :param locale_in_transition: Include the locale in the transition URL :return: string url """ # no longer used, but still passed in. leaving here for now # as it will likely be used in future. # _version = version _locale = 'ja-JP-mac' if platform == 'osx' and locale == 'ja' else locale channel = 'devedition' if channel == 'alpha' else channel force_direct = True if channel != 'release' else force_direct stub_platforms = ['win', 'win64'] esr_channels = ['esr', 'esr_next'] include_funnelcake_param = False # support optional MSI installer downloads # bug 1493205 is_msi = platform.endswith('-msi') if is_msi: platform = platform[:-4] # Bug 1345467 - Only allow specifically configured funnelcake builds if funnelcake_id: fc_platforms = config('FUNNELCAKE_%s_PLATFORMS' % funnelcake_id, default='', parser=ListOf(str)) fc_locales = config('FUNNELCAKE_%s_LOCALES' % funnelcake_id, default='', parser=ListOf(str)) include_funnelcake_param = platform in fc_platforms and _locale in fc_locales # Check if direct download link has been requested # if not just use transition URL if not force_direct: # build a link to the transition page transition_url = self.download_base_url_transition if funnelcake_id: # include funnelcake in scene 2 URL transition_url += '?f=%s' % funnelcake_id if locale_in_transition: transition_url = '/%s%s' % (locale, transition_url) return transition_url # otherwise build a full download URL prod_name = 'firefox' if channel == 'release' else 'firefox-%s' % channel suffix = 'latest-ssl' if is_msi: suffix = 'msi-' + suffix if channel in esr_channels: # nothing special about ESR other than there is no stub. # included in this contitional to avoid the following elif. if channel == 'esr_next': prod_name = 'firefox-esr-next' elif platform in stub_platforms and not is_msi and not force_full_installer: # Use the stub installer for approved platforms # append funnelcake id to version if we have one if include_funnelcake_param: suffix = 'stub-f%s' % funnelcake_id else: suffix = 'stub' elif channel == 'nightly' and locale != 'en-US': # Nightly uses a different product name for localized builds, # and is the only one ಠ_ಠ suffix = 'latest-l10n-ssl' if is_msi: suffix = 'msi-' + suffix product = '%s-%s' % (prod_name, suffix) return '?'.join([ self.bouncer_url, urlencode([ ('product', product), ('os', platform), # Order matters, lang must be last for bouncer. ('lang', _locale), ]) ])
} SITEMAPS_REPO = config( 'SITEMAPS_REPO', default='https://github.com/mozmeao/www-sitemap-generator.git') SITEMAPS_PATH = DATA_PATH / 'sitemaps' # Pages that have different URLs for different locales, e.g. # 'firefox/private-browsing/': { # 'en-US': '/firefox/features/private-browsing/', # }, ALT_CANONICAL_PATHS = {} ALLOWED_HOSTS = config( 'ALLOWED_HOSTS', parser=ListOf(str), default='www.mozilla.org,www.ipv6.mozilla.org,www.allizom.org') ALLOWED_CIDR_NETS = config('ALLOWED_CIDR_NETS', default='', parser=ListOf(str)) # The canonical, production URL without a trailing slash CANONICAL_URL = 'https://www.mozilla.org' # Make this unique, and don't share it with anybody. SECRET_KEY = config('SECRET_KEY', default='ssssshhhhh') MEDIA_URL = config('MEDIA_URL', default='/user-media/') MEDIA_ROOT = config('MEDIA_ROOT', default=path('media')) STATIC_URL = config('STATIC_URL', default='/media/') STATIC_ROOT = config('STATIC_ROOT', default=path('static')) STATICFILES_STORAGE = ( 'django.contrib.staticfiles.storage.StaticFilesStorage' if DEBUG else
'/privacy/firefox-klar/': ['de'], '/about/legal/impressum/': ['de'], } SITEMAPS_REPO = config('SITEMAPS_REPO', default='https://github.com/mozmeao/www-sitemap-generator.git') SITEMAPS_PATH = DATA_PATH / 'sitemaps' # Pages that have different URLs for different locales, e.g. # 'firefox/private-browsing/': { # 'en-US': '/firefox/features/private-browsing/', # }, ALT_CANONICAL_PATHS = {} ALLOWED_HOSTS = config( 'ALLOWED_HOSTS', parser=ListOf(str), default='www.mozilla.org,www.ipv6.mozilla.org,www.allizom.org') ALLOWED_CIDR_NETS = config('ALLOWED_CIDR_NETS', default='', parser=ListOf(str)) # The canonical, production URL without a trailing slash CANONICAL_URL = 'https://www.mozilla.org' # Make this unique, and don't share it with anybody. SECRET_KEY = config('SECRET_KEY', default='ssssshhhhh') MEDIA_URL = config('MEDIA_URL', default='/user-media/') MEDIA_ROOT = config('MEDIA_ROOT', default=path('media')) STATIC_URL = config('STATIC_URL', default='/media/') STATIC_ROOT = config('STATIC_ROOT', default=path('static')) STATICFILES_STORAGE = ('django.contrib.staticfiles.storage.StaticFilesStorage' if DEBUG else 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage')
]) # Build paths inside the project like this: path(...) BASE_DIR = str(ROOT_PATH) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = config('SECRET_KEY') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = config('DEBUG', default='false', parser=bool) ALLOWED_HOSTS = config('ALLOWED_HOSTS', parser=ListOf(str), default='localhost') SITE_TITLE = config('SITE_TITLE', default='standup') # Application definition INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'django_jinja', 'django_jinja.contrib._humanize', 'django_jinja_markdown',
# Pages we do want indexed but don't show up in automated URL discovery # or are only available in a non-default locale EXTRA_INDEX_URLS = [ '/de/privacy/firefox-klar/', '/de/about/legal/impressum/', ] # Pages that have different URLs for different locales, e.g. # 'firefox/private-browsing/': { # 'en-US': '/firefox/features/private-browsing/', # }, ALT_CANONICAL_PATHS = {} ALLOWED_HOSTS = config( 'ALLOWED_HOSTS', parser=ListOf(str), default='www.mozilla.org,www.ipv6.mozilla.org,www.allizom.org') ALLOWED_CIDR_NETS = config('ALLOWED_CIDR_NETS', default='', parser=ListOf(str)) # The canonical, production URL without a trailing slash CANONICAL_URL = 'https://www.mozilla.org' # Make this unique, and don't share it with anybody. SECRET_KEY = config('SECRET_KEY', default='ssssshhhhh') MEDIA_URL = config('MEDIA_URL', default='/user-media/') MEDIA_ROOT = config('MEDIA_ROOT', default=path('media')) STATIC_URL = config('STATIC_URL', default='/media/') STATIC_ROOT = config('STATIC_ROOT', default=path('static')) STATICFILES_STORAGE = ( 'django.contrib.staticfiles.storage.StaticFilesStorage' if DEBUG else
def test_ListOf(): assert ListOf(str)("") == [] assert ListOf(str)("foo") == ["foo"] assert ListOf(bool)("t,f") == [True, False] assert ListOf(int)("1,2,3") == [1, 2, 3] assert ListOf(int, delimiter=":")("1:2") == [1, 2]
class AppConfig(RequiredConfigMixin): """Application-level config. To pull out a config item, you can do this:: config = ConfigManager([ConfigOSEnv()]) app_config = AppConfig(config) debug = app_config('debug') To create a component with configuration, you can do this:: class SomeComponent(RequiredConfigMixin): required_config = ConfigOptions() def __init__(self, config): self.config = config.with_options(self) some_component = SomeComponent(app_config.config) To pass application-level configuration to components, you should do it through arguments like this:: class SomeComponent(RequiredConfigMixin): required_config = ConfigOptions() def __init__(self, config, debug): self.config = config.with_options(self) self.debug = debug some_component = SomeComponent(app_config.config_manager, debug) """ required_config = ConfigOptions() required_config.add_option( "basedir", default=str(Path(__file__).parent.parent), doc="The root directory for this application to find and store things.", ) required_config.add_option( "logging_level", default="DEBUG", doc="The logging level to use. DEBUG, INFO, WARNING, ERROR or CRITICAL", ) required_config.add_option( "local_dev_env", default="False", parser=bool, doc="Whether or not this is a local development environment.", ) required_config.add_option( "metrics_class", default="antenna.metrics.LoggingMetrics", doc=( "Comma-separated list of metrics backends to use. Possible options: " '"antenna.metrics.LoggingMetrics" and "antenna.metrics.DatadogMetrics"', ), parser=ListOf(parse_class), ) required_config.add_option( "secret_sentry_dsn", default="", doc=( "Sentry DSN to use. See https://docs.sentry.io/quickstart/#configure-the-dsn " "for details. If this is not set an unhandled exception logging middleware " "will be used instead." ), ) required_config.add_option( "host_id", default="", doc=( "Identifier for the host that is running Antenna. This identifies this Antenna " "instance in the logs and makes it easier to correlate Antenna logs with " "other data. For example, the value could be a public hostname, an instance id, " "or something like that. If you do not set this, then socket.gethostname() is " "used instead." ), ) def __init__(self, config): self.config_manager = config self.config = config.with_options(self) def __call__(self, key): """Return configuration for given key.""" return self.config(key)
'SECRET_KEY': 'vo7v#s9o7$t%x=fpbt7j5#%=-bl^y6e4&n2hpklg&rzx%z2mp$', 'DEBUG': 'false', }) ]) # Core config BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ADMINS = (('Kshitij Sobti', '*****@*****.**'), ) DEBUG = config('DEBUG', parser=bool) SECRET_KEY = config('SECRET_KEY') ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='', parser=ListOf(str)) # Cors config CORS_ORIGIN_ALLOW_ALL = config('ORIGIN_ALLOW_ALL', namespace='cors', default=str(DEBUG), parser=bool) CORS_ORIGIN_WHITELIST = config('ORIGIN_WHITELIST', namespace='cors', default='', parser=ListOf(str)) # Database config
class AppConfig(RequiredConfigMixin): """Application-level config To pull out a config item, you can do this:: config = ConfigManager([ConfigOSEnv()]) app_config = AppConfig(config) debug = app_config('debug') To create a component with configuration, you can do this:: class SomeComponent(RequiredConfigMixin): required_config = ConfigOptions() def __init__(self, config): self.config = config.with_options(self) some_component = SomeComponent(app_config.config) To pass application-level configuration to components, you should do it through arguments like this:: class SomeComponent(RequiredConfigMixin): required_config = ConfigOptions() def __init__(self, config, debug): self.config = config.with_options(self) self.debug = debug some_component = SomeComponent(app_config.config_manager, debug) """ required_config = ConfigOptions() required_config.add_option( 'basedir', default=str(Path(__file__).parent.parent), doc='The root directory for this application to find and store things.' ) required_config.add_option( 'logging_level', default='DEBUG', doc='The logging level to use. DEBUG, INFO, WARNING, ERROR or CRITICAL' ) required_config.add_option( 'metrics_class', default='antenna.metrics.LoggingMetrics', doc=( 'Comma-separated list of metrics backends to use. Possible options: ' '"antenna.metrics.LoggingMetrics" and "antenna.metrics.DatadogMetrics"', ), parser=ListOf(parse_class) ) required_config.add_option( 'secret_sentry_dsn', default='', doc=( 'Sentry DSN to use. See https://docs.sentry.io/quickstart/#configure-the-dsn ' 'for details. If this is not set an unhandled exception logging middleware ' 'will be used instead.' ) ) required_config.add_option( 'host_id', default='', doc=( 'Identifier for the host that is running Antenna. This identifies this Antenna ' 'instance in the logs and makes it easier to correlate Antenna logs with ' 'other data. For example, the value could be a public hostname, an instance id, ' 'or something like that. If you do not set this, then socket.gethostname() is ' 'used instead.' ) ) def __init__(self, config): self.config_manager = config self.config = config.with_options(self) def __call__(self, key): return self.config(key)
]) # Build paths inside the project like this: path(...) BASE_DIR = str(ROOT_PATH) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = config('SECRET_KEY') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = config('DEBUG', default='false', parser=bool) ALLOWED_HOSTS = config('ALLOWED_HOSTS', parser=ListOf(str), default='localhost') ENFORCE_HOSTNAME = config('ENFORCE_HOSTNAME', parser=ListOf(str), raise_error=False) SITE_TITLE = config('SITE_TITLE', default='Standup') # Application definition INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'django_jinja', 'django_jinja.contrib._humanize',
def get_download_url( self, channel, version, platform, locale, force_direct=False, force_full_installer=False, force_funnelcake=False, funnelcake_id=None, locale_in_transition=False, ): """ Get direct download url for the product. :param channel: one of self.version_map.keys(). :param version: a firefox version. one of self.latest_version. :param platform: OS. one of self.platform_labels.keys(). :param locale: e.g. pt-BR. one exception is ja-JP-mac. :param force_direct: Force the download URL to be direct. always True for non-release URLs. :param force_full_installer: Force the installer download to not be the stub installer (for aurora). :param force_funnelcake: Force the download version for en-US Windows to be 'latest', which bouncer will translate to the funnelcake build. :param funnelcake_id: ID for the the funnelcake build. :param locale_in_transition: Include the locale in the transition URL :return: string url """ # no longer used, but still passed in. leaving here for now # as it will likely be used in future. # _version = version _locale = "ja-JP-mac" if platform == "osx" and locale == "ja" else locale channel = "devedition" if channel == "alpha" else channel force_direct = True if channel != "release" else force_direct stub_platforms = ["win", "win64"] esr_channels = ["esr", "esr_next"] include_funnelcake_param = False # support optional MSI installer downloads # bug 1493205 is_msi = platform.endswith("-msi") if is_msi: platform = platform[:-4] # Bug 1345467 - Only allow specifically configured funnelcake builds if funnelcake_id: fc_platforms = config(f"FUNNELCAKE_{funnelcake_id}_PLATFORMS", default="", parser=ListOf(str)) fc_locales = config(f"FUNNELCAKE_{funnelcake_id}_LOCALES", default="", parser=ListOf(str)) include_funnelcake_param = platform in fc_platforms and _locale in fc_locales # Check if direct download link has been requested # if not just use transition URL if not force_direct: # build a link to the transition page transition_url = self.download_base_url_transition if funnelcake_id: # include funnelcake in scene 2 URL transition_url += f"?f={funnelcake_id}" if locale_in_transition: transition_url = f"/{locale}{transition_url}" return transition_url # otherwise build a full download URL prod_name = "firefox" if channel == "release" else f"firefox-{channel}" suffix = "latest-ssl" if is_msi: suffix = "msi-" + suffix if channel in esr_channels: # nothing special about ESR other than there is no stub. # included in this contitional to avoid the following elif. if channel == "esr_next": prod_name = "firefox-esr-next" elif platform in stub_platforms and not is_msi and not force_full_installer: # Use the stub installer for approved platforms # append funnelcake id to version if we have one if include_funnelcake_param: suffix = f"stub-f{funnelcake_id}" else: suffix = "stub" elif channel == "nightly" and locale != "en-US": # Nightly uses a different product name for localized builds, # and is the only one ಠ_ಠ suffix = "latest-l10n-ssl" if is_msi: suffix = "msi-" + suffix product = f"{prod_name}-{suffix}" return "?".join([ self.bouncer_url, urlencode([ ("product", product), ("os", platform), # Order matters, lang must be last for bouncer. ("lang", _locale), ]), ])
]) # Build paths inside the project like this: path(...) BASE_DIR = str(ROOT_PATH) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = config('SECRET_KEY') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = config('DEBUG', default='false', parser=bool) ALLOWED_HOSTS = config('ALLOWED_HOSTS', parser=ListOf(str), default='localhost') ENFORCE_HOSTNAME = config('ENFORCE_HOSTNAME', parser=ListOf(str), raise_error=False) SITE_TITLE = config('SITE_TITLE', default='Standup') # Application definition INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'django_jinja',
]) LOG_LEVEL = config('LOG_LEVEL', default='INFO') SECRET_KEY = str(uuid4()) GITHUB_SECRET = config('GITHUB_SECRET', raise_error=False) SSH_DOKKU_HOST = config('SSH_DOKKU_HOST', raise_error=False) SSH_DOKKU_PORT = config('SSH_DOKKU_PORT', parser=int, default='22') SSH_DOKKU_USER = config('SSH_DOKKU_USER', default='dokku') APPS_DOKKU_DOMAIN = config('APPS_DOKKU_DOMAIN', default=SSH_DOKKU_HOST or '') APPS_LETSENCRYPT = config('APPS_LETSENCRYPT', parser=bool, default='False') # the prefix of the branch names to deploy as demos DEMO_BRANCH_PREFIX = config('DEMO_BRANCH_PREFIX', default='demo/') # default extra branch names to deploy # can be overridden by repo specific config: `<owner>_<repo>_BRANCHES` DEFAULT_DEPLOY_BRANCHES = config('DEFAULT_DEPLOY_BRANCHES', parser=ListOf(str), default='') # use `repo_full_name` to include github owner in name APP_NAME_TEMPLATE = config('APP_NAME_TEMPLATE', default='{repo_name}-{branch_name}') # notifications stuff # configure these if you want slack notifications # see SLACK_API_TOKEN = config('SLACK_API_TOKEN', raise_error=False) SLACK_CHANNEL = config('SLACK_CHANNEL', raise_error=False) def get_deploy_branches(repo_full_name): """ Get repo specific branch list to deploy if configured. :return: list of branch names
# Pages we do want indexed but don't show up in automated URL discovery # or are only available in a non-default locale EXTRA_INDEX_URLS = { '/privacy/firefox-klar/': ['de'], '/about/legal/impressum/': ['de'], } # Pages that have different URLs for different locales, e.g. # 'firefox/private-browsing/': { # 'en-US': '/firefox/features/private-browsing/', # }, ALT_CANONICAL_PATHS = {} ALLOWED_HOSTS = config( 'ALLOWED_HOSTS', parser=ListOf(str), default='www.mozilla.org,www.ipv6.mozilla.org,www.allizom.org') ALLOWED_CIDR_NETS = config('ALLOWED_CIDR_NETS', default='', parser=ListOf(str)) # The canonical, production URL without a trailing slash CANONICAL_URL = 'https://www.mozilla.org' # Make this unique, and don't share it with anybody. SECRET_KEY = config('SECRET_KEY', default='ssssshhhhh') MEDIA_URL = config('MEDIA_URL', default='/user-media/') MEDIA_ROOT = config('MEDIA_ROOT', default=path('media')) STATIC_URL = config('STATIC_URL', default='/media/') STATIC_ROOT = config('STATIC_ROOT', default=path('static')) STATICFILES_STORAGE = ('django.contrib.staticfiles.storage.StaticFilesStorage' if DEBUG else 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage')
def test_ListOf(): assert ListOf(str)('') == [] assert ListOf(str)('foo') == ['foo'] assert ListOf(bool)('t,f') == [True, False] assert ListOf(int)('1,2,3') == [1, 2, 3] assert ListOf(int, delimiter=':')('1:2') == [1, 2]