| #!/usr/bin/env python |
| # |
| # Copyright 2007 Google Inc. |
| # |
| # 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. |
| # |
| """Stores application configuration taken from e.g. app.yaml, queues.yaml.""" |
| |
| # TODO: Support more than just app.yaml. |
| |
| |
| import errno |
| import logging |
| import os |
| import os.path |
| import random |
| import string |
| import threading |
| import types |
| |
| from google.appengine.api import appinfo |
| from google.appengine.api import appinfo_includes |
| from google.appengine.api import backendinfo |
| from google.appengine.api import dispatchinfo |
| from google.appengine.tools import yaml_translator |
| from google.appengine.tools.devappserver2 import errors |
| |
| # Constants passed to functions registered with |
| # ModuleConfiguration.add_change_callback. |
| NORMALIZED_LIBRARIES_CHANGED = 1 |
| SKIP_FILES_CHANGED = 2 |
| HANDLERS_CHANGED = 3 |
| INBOUND_SERVICES_CHANGED = 4 |
| ENV_VARIABLES_CHANGED = 5 |
| ERROR_HANDLERS_CHANGED = 6 |
| NOBUILD_FILES_CHANGED = 7 |
| |
| |
| def java_supported(): |
| """True if this SDK supports running Java apps in the dev appserver.""" |
| java_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'java') |
| return os.path.isdir(java_dir) |
| |
| |
| class ModuleConfiguration(object): |
| """Stores module configuration information. |
| |
| Most configuration options are mutable and may change any time |
| check_for_updates is called. Client code must be able to cope with these |
| changes. |
| |
| Other properties are immutable (see _IMMUTABLE_PROPERTIES) and are guaranteed |
| to be constant for the lifetime of the instance. |
| """ |
| |
| _IMMUTABLE_PROPERTIES = [ |
| ('application', 'application'), |
| ('version', 'major_version'), |
| ('runtime', 'runtime'), |
| ('threadsafe', 'threadsafe'), |
| ('module', 'module_name'), |
| ('basic_scaling', 'basic_scaling'), |
| ('manual_scaling', 'manual_scaling'), |
| ('automatic_scaling', 'automatic_scaling')] |
| |
| def __init__(self, config_path): |
| """Initializer for ModuleConfiguration. |
| |
| Args: |
| config_path: A string containing the full path of the yaml or xml file |
| containing the configuration for this module. |
| """ |
| self._config_path = config_path |
| root = os.path.dirname(config_path) |
| self._is_java = os.path.normpath(config_path).endswith( |
| os.sep + 'WEB-INF' + os.sep + 'appengine-web.xml') |
| if self._is_java: |
| # We assume Java's XML-based config files only if config_path is |
| # something like /foo/bar/WEB-INF/appengine-web.xml. In this case, |
| # the application root is /foo/bar. Other apps, configured with YAML, |
| # have something like /foo/bar/app.yaml, with application root /foo/bar. |
| root = os.path.dirname(root) |
| self._application_root = os.path.realpath(root) |
| self._last_failure_message = None |
| |
| self._app_info_external, files_to_check = self._parse_configuration( |
| self._config_path) |
| self._mtimes = self._get_mtimes(files_to_check) |
| self._application = '%s~%s' % (self.partition, |
| self.application_external_name) |
| self._api_version = self._app_info_external.api_version |
| self._module_name = self._app_info_external.module |
| self._version = self._app_info_external.version |
| self._threadsafe = self._app_info_external.threadsafe |
| self._basic_scaling = self._app_info_external.basic_scaling |
| self._manual_scaling = self._app_info_external.manual_scaling |
| self._automatic_scaling = self._app_info_external.automatic_scaling |
| self._runtime = self._app_info_external.runtime |
| if self._runtime == 'python': |
| logging.warning( |
| 'The "python" runtime specified in "%s" is not supported - the ' |
| '"python27" runtime will be used instead. A description of the ' |
| 'differences between the two can be found here:\n' |
| 'https://developers.google.com/appengine/docs/python/python25/diff27', |
| self._config_path) |
| self._minor_version_id = ''.join(random.choice(string.digits) for _ in |
| range(18)) |
| |
| @property |
| def application_root(self): |
| """The directory containing the application e.g. "/home/user/myapp".""" |
| return self._application_root |
| |
| @property |
| def application(self): |
| return self._application |
| |
| @property |
| def partition(self): |
| return 'dev' |
| |
| @property |
| def application_external_name(self): |
| return self._app_info_external.application |
| |
| @property |
| def api_version(self): |
| return self._api_version |
| |
| @property |
| def module_name(self): |
| return self._module_name or appinfo.DEFAULT_MODULE |
| |
| @property |
| def major_version(self): |
| return self._version |
| |
| @property |
| def minor_version(self): |
| return self._minor_version_id |
| |
| @property |
| def version_id(self): |
| if self.module_name == appinfo.DEFAULT_MODULE: |
| return '%s.%s' % ( |
| self.major_version, |
| self._minor_version_id) |
| else: |
| return '%s:%s.%s' % ( |
| self.module_name, |
| self.major_version, |
| self._minor_version_id) |
| |
| @property |
| def runtime(self): |
| return self._runtime |
| |
| @property |
| def effective_runtime(self): |
| return self._app_info_external.GetEffectiveRuntime() |
| |
| @property |
| def threadsafe(self): |
| return self._threadsafe |
| |
| @property |
| def basic_scaling(self): |
| return self._basic_scaling |
| |
| @property |
| def manual_scaling(self): |
| return self._manual_scaling |
| |
| @property |
| def automatic_scaling(self): |
| return self._automatic_scaling |
| |
| @property |
| def normalized_libraries(self): |
| return self._app_info_external.GetNormalizedLibraries() |
| |
| @property |
| def skip_files(self): |
| return self._app_info_external.skip_files |
| |
| @property |
| def nobuild_files(self): |
| return self._app_info_external.nobuild_files |
| |
| @property |
| def error_handlers(self): |
| return self._app_info_external.error_handlers |
| |
| @property |
| def handlers(self): |
| return self._app_info_external.handlers |
| |
| @property |
| def inbound_services(self): |
| return self._app_info_external.inbound_services |
| |
| @property |
| def env_variables(self): |
| return self._app_info_external.env_variables |
| |
| @property |
| def is_backend(self): |
| return False |
| |
| def check_for_updates(self): |
| """Return any configuration changes since the last check_for_updates call. |
| |
| Returns: |
| A set containing the changes that occured. See the *_CHANGED module |
| constants. |
| """ |
| new_mtimes = self._get_mtimes(self._mtimes.keys()) |
| if new_mtimes == self._mtimes: |
| return set() |
| |
| try: |
| app_info_external, files_to_check = self._parse_configuration( |
| self._config_path) |
| except Exception, e: |
| failure_message = str(e) |
| if failure_message != self._last_failure_message: |
| logging.error('Configuration is not valid: %s', failure_message) |
| self._last_failure_message = failure_message |
| return set() |
| self._last_failure_message = None |
| |
| self._mtimes = self._get_mtimes(files_to_check) |
| |
| for app_info_attribute, self_attribute in self._IMMUTABLE_PROPERTIES: |
| app_info_value = getattr(app_info_external, app_info_attribute) |
| self_value = getattr(self, self_attribute) |
| if (app_info_value == self_value or |
| app_info_value == getattr(self._app_info_external, |
| app_info_attribute)): |
| # Only generate a warning if the value is both different from the |
| # immutable value *and* different from the last loaded value. |
| continue |
| |
| if isinstance(app_info_value, types.StringTypes): |
| logging.warning('Restart the development module to see updates to "%s" ' |
| '["%s" => "%s"]', |
| app_info_attribute, |
| self_value, |
| app_info_value) |
| else: |
| logging.warning('Restart the development module to see updates to "%s"', |
| app_info_attribute) |
| |
| changes = set() |
| if (app_info_external.GetNormalizedLibraries() != |
| self.normalized_libraries): |
| changes.add(NORMALIZED_LIBRARIES_CHANGED) |
| if app_info_external.skip_files != self.skip_files: |
| changes.add(SKIP_FILES_CHANGED) |
| if app_info_external.nobuild_files != self.nobuild_files: |
| changes.add(NOBUILD_FILES_CHANGED) |
| if app_info_external.handlers != self.handlers: |
| changes.add(HANDLERS_CHANGED) |
| if app_info_external.inbound_services != self.inbound_services: |
| changes.add(INBOUND_SERVICES_CHANGED) |
| if app_info_external.env_variables != self.env_variables: |
| changes.add(ENV_VARIABLES_CHANGED) |
| if app_info_external.error_handlers != self.error_handlers: |
| changes.add(ERROR_HANDLERS_CHANGED) |
| |
| self._app_info_external = app_info_external |
| if changes: |
| self._minor_version_id = ''.join(random.choice(string.digits) for _ in |
| range(18)) |
| return changes |
| |
| @staticmethod |
| def _get_mtimes(filenames): |
| filename_to_mtime = {} |
| for filename in filenames: |
| try: |
| filename_to_mtime[filename] = os.path.getmtime(filename) |
| except OSError as e: |
| # Ignore deleted includes. |
| if e.errno != errno.ENOENT: |
| raise |
| return filename_to_mtime |
| |
| def _parse_configuration(self, configuration_path): |
| """Parse a configuration file (like app.yaml or appengine-web.xml). |
| |
| Args: |
| configuration_path: A string containing the full path of the yaml file |
| containing the configuration for this module. |
| |
| Returns: |
| A tuple where the first element is the parsed appinfo.AppInfoExternal |
| object and the second element is a list of the paths of the files that |
| were used to produce it, namely the input configuration_path and any |
| other file that was included from that one. |
| """ |
| if self._is_java: |
| config, files = self._parse_java_configuration(configuration_path) |
| else: |
| with open(configuration_path) as f: |
| config, files = appinfo_includes.ParseAndReturnIncludePaths(f) |
| return config, [configuration_path] + files |
| |
| @staticmethod |
| def _parse_java_configuration(app_engine_web_xml_path): |
| """Parse appengine-web.xml and web.xml. |
| |
| Args: |
| app_engine_web_xml_path: A string containing the full path of the |
| .../WEB-INF/appengine-web.xml file. The corresponding |
| .../WEB-INF/web.xml file must also be present. |
| |
| Returns: |
| A tuple where the first element is the parsed appinfo.AppInfoExternal |
| object and the second element is a list of the paths of the files that |
| were used to produce it, namely the input appengine-web.xml file and the |
| corresponding web.xml file. |
| """ |
| with open(app_engine_web_xml_path) as f: |
| app_engine_web_xml_str = f.read() |
| web_inf_dir = os.path.dirname(app_engine_web_xml_path) |
| web_xml_path = os.path.join(web_inf_dir, 'web.xml') |
| with open(web_xml_path) as f: |
| web_xml_str = f.read() |
| static_files = [] |
| # TODO: need to enumerate static files here |
| app_yaml_str = yaml_translator.TranslateXmlToYaml( |
| app_engine_web_xml_str, web_xml_str, static_files) |
| config = appinfo.LoadSingleAppInfo(app_yaml_str) |
| return config, [app_engine_web_xml_path, web_xml_path] |
| |
| |
| class BackendsConfiguration(object): |
| """Stores configuration information for a backends.yaml file.""" |
| |
| def __init__(self, app_config_path, backend_config_path): |
| """Initializer for BackendsConfiguration. |
| |
| Args: |
| app_config_path: A string containing the full path of the yaml file |
| containing the configuration for this module. |
| backend_config_path: A string containing the full path of the |
| backends.yaml file containing the configuration for backends. |
| """ |
| self._update_lock = threading.RLock() |
| self._base_module_configuration = ModuleConfiguration(app_config_path) |
| backend_info_external = self._parse_configuration( |
| backend_config_path) |
| |
| self._backends_name_to_backend_entry = {} |
| for backend in backend_info_external.backends or []: |
| self._backends_name_to_backend_entry[backend.name] = backend |
| self._changes = dict( |
| (backend_name, set()) |
| for backend_name in self._backends_name_to_backend_entry) |
| |
| @staticmethod |
| def _parse_configuration(configuration_path): |
| # TODO: It probably makes sense to catch the exception raised |
| # by Parse() and re-raise it using a module-specific exception. |
| with open(configuration_path) as f: |
| return backendinfo.LoadBackendInfo(f) |
| |
| def get_backend_configurations(self): |
| return [BackendConfiguration(self._base_module_configuration, self, entry) |
| for entry in self._backends_name_to_backend_entry.values()] |
| |
| def check_for_updates(self, backend_name): |
| """Return any configuration changes since the last check_for_updates call. |
| |
| Args: |
| backend_name: A str containing the name of the backend to be checked for |
| updates. |
| |
| Returns: |
| A set containing the changes that occured. See the *_CHANGED module |
| constants. |
| """ |
| with self._update_lock: |
| module_changes = self._base_module_configuration.check_for_updates() |
| if module_changes: |
| for backend_changes in self._changes.values(): |
| backend_changes.update(module_changes) |
| changes = self._changes[backend_name] |
| self._changes[backend_name] = set() |
| return changes |
| |
| |
| class BackendConfiguration(object): |
| """Stores backend configuration information. |
| |
| This interface is and must remain identical to ModuleConfiguration. |
| """ |
| |
| def __init__(self, module_configuration, backends_configuration, |
| backend_entry): |
| """Initializer for BackendConfiguration. |
| |
| Args: |
| module_configuration: A ModuleConfiguration to use. |
| backends_configuration: The BackendsConfiguration that tracks updates for |
| this BackendConfiguration. |
| backend_entry: A backendinfo.BackendEntry containing the backend |
| configuration. |
| """ |
| self._module_configuration = module_configuration |
| self._backends_configuration = backends_configuration |
| self._backend_entry = backend_entry |
| |
| if backend_entry.dynamic: |
| self._basic_scaling = appinfo.BasicScaling( |
| max_instances=backend_entry.instances or 1) |
| self._manual_scaling = None |
| else: |
| self._basic_scaling = None |
| self._manual_scaling = appinfo.ManualScaling( |
| instances=backend_entry.instances or 1) |
| self._minor_version_id = ''.join(random.choice(string.digits) for _ in |
| range(18)) |
| |
| @property |
| def application_root(self): |
| """The directory containing the application e.g. "/home/user/myapp".""" |
| return self._module_configuration.application_root |
| |
| @property |
| def application(self): |
| return self._module_configuration.application |
| |
| @property |
| def partition(self): |
| return self._module_configuration.partition |
| |
| @property |
| def application_external_name(self): |
| return self._module_configuration.application_external_name |
| |
| @property |
| def api_version(self): |
| return self._module_configuration.api_version |
| |
| @property |
| def module_name(self): |
| return self._backend_entry.name |
| |
| @property |
| def major_version(self): |
| return self._module_configuration.major_version |
| |
| @property |
| def minor_version(self): |
| return self._minor_version_id |
| |
| @property |
| def version_id(self): |
| return '%s:%s.%s' % ( |
| self.module_name, |
| self.major_version, |
| self._minor_version_id) |
| |
| @property |
| def runtime(self): |
| return self._module_configuration.runtime |
| |
| @property |
| def effective_runtime(self): |
| return self._module_configuration.effective_runtime |
| |
| @property |
| def threadsafe(self): |
| return self._module_configuration.threadsafe |
| |
| @property |
| def basic_scaling(self): |
| return self._basic_scaling |
| |
| @property |
| def manual_scaling(self): |
| return self._manual_scaling |
| |
| @property |
| def automatic_scaling(self): |
| return None |
| |
| @property |
| def normalized_libraries(self): |
| return self._module_configuration.normalized_libraries |
| |
| @property |
| def skip_files(self): |
| return self._module_configuration.skip_files |
| |
| @property |
| def nobuild_files(self): |
| return self._module_configuration.nobuild_files |
| |
| @property |
| def error_handlers(self): |
| return self._module_configuration.error_handlers |
| |
| @property |
| def handlers(self): |
| if self._backend_entry.start: |
| return [appinfo.URLMap( |
| url='/_ah/start', |
| script=self._backend_entry.start, |
| login='admin')] + self._module_configuration.handlers |
| return self._module_configuration.handlers |
| |
| @property |
| def inbound_services(self): |
| return self._module_configuration.inbound_services |
| |
| @property |
| def env_variables(self): |
| return self._module_configuration.env_variables |
| |
| @property |
| def is_backend(self): |
| return True |
| |
| def check_for_updates(self): |
| """Return any configuration changes since the last check_for_updates call. |
| |
| Returns: |
| A set containing the changes that occured. See the *_CHANGED module |
| constants. |
| """ |
| changes = self._backends_configuration.check_for_updates( |
| self._backend_entry.name) |
| if changes: |
| self._minor_version_id = ''.join(random.choice(string.digits) for _ in |
| range(18)) |
| return changes |
| |
| |
| class DispatchConfiguration(object): |
| """Stores dispatcher configuration information.""" |
| |
| def __init__(self, config_path): |
| self._config_path = config_path |
| self._mtime = os.path.getmtime(self._config_path) |
| self._process_dispatch_entries(self._parse_configuration(self._config_path)) |
| |
| @staticmethod |
| def _parse_configuration(configuration_path): |
| # TODO: It probably makes sense to catch the exception raised |
| # by LoadSingleDispatch() and re-raise it using a module-specific exception. |
| with open(configuration_path) as f: |
| return dispatchinfo.LoadSingleDispatch(f) |
| |
| def check_for_updates(self): |
| mtime = os.path.getmtime(self._config_path) |
| if mtime > self._mtime: |
| self._mtime = mtime |
| try: |
| dispatch_info_external = self._parse_configuration(self._config_path) |
| except Exception, e: |
| failure_message = str(e) |
| logging.error('Configuration is not valid: %s', failure_message) |
| return |
| self._process_dispatch_entries(dispatch_info_external) |
| |
| def _process_dispatch_entries(self, dispatch_info_external): |
| path_only_entries = [] |
| hostname_entries = [] |
| for entry in dispatch_info_external.dispatch: |
| parsed_url = dispatchinfo.ParsedURL(entry.url) |
| if parsed_url.host: |
| hostname_entries.append(entry) |
| else: |
| path_only_entries.append((parsed_url, entry.module)) |
| if hostname_entries: |
| logging.warning( |
| 'Hostname routing is not supported by the development server. The ' |
| 'following dispatch entries will not match any requests:\n%s', |
| '\n\t'.join(str(entry) for entry in hostname_entries)) |
| self._entries = path_only_entries |
| |
| @property |
| def dispatch(self): |
| return self._entries |
| |
| |
| class ApplicationConfiguration(object): |
| """Stores application configuration information.""" |
| |
| def __init__(self, config_paths): |
| """Initializer for ApplicationConfiguration. |
| |
| Args: |
| config_paths: A list of strings containing the paths to yaml files, |
| or to directories containing them. |
| """ |
| self.modules = [] |
| self.dispatch = None |
| # It's really easy to add a test case that passes in a string rather than |
| # a list of strings, so guard against that. |
| assert not isinstance(config_paths, basestring) |
| config_paths = self._config_files_from_paths(config_paths) |
| for config_path in config_paths: |
| # TODO: add support for backends.xml and dispatch.xml here |
| if (config_path.endswith('backends.yaml') or |
| config_path.endswith('backends.yml')): |
| # TODO: Reuse the ModuleConfiguration created for the app.yaml |
| # instead of creating another one for the same file. |
| self.modules.extend( |
| BackendsConfiguration(config_path.replace('backends.y', 'app.y'), |
| config_path).get_backend_configurations()) |
| elif (config_path.endswith('dispatch.yaml') or |
| config_path.endswith('dispatch.yml')): |
| if self.dispatch: |
| raise errors.InvalidAppConfigError( |
| 'Multiple dispatch.yaml files specified') |
| self.dispatch = DispatchConfiguration(config_path) |
| else: |
| module_configuration = ModuleConfiguration(config_path) |
| self.modules.append(module_configuration) |
| application_ids = set(module.application |
| for module in self.modules) |
| if len(application_ids) > 1: |
| raise errors.InvalidAppConfigError( |
| 'More than one application ID found: %s' % |
| ', '.join(sorted(application_ids))) |
| |
| self._app_id = application_ids.pop() |
| module_names = set() |
| for module in self.modules: |
| if module.module_name in module_names: |
| raise errors.InvalidAppConfigError('Duplicate module: %s' % |
| module.module_name) |
| module_names.add(module.module_name) |
| if self.dispatch: |
| if appinfo.DEFAULT_MODULE not in module_names: |
| raise errors.InvalidAppConfigError( |
| 'A default module must be specified.') |
| missing_modules = ( |
| set(module_name for _, module_name in self.dispatch.dispatch) - |
| module_names) |
| if missing_modules: |
| raise errors.InvalidAppConfigError( |
| 'Modules %s specified in dispatch.yaml are not defined by a yaml ' |
| 'file.' % sorted(missing_modules)) |
| |
| def _config_files_from_paths(self, config_paths): |
| """Return a list of the configuration files found in the given paths. |
| |
| For any path that is a directory, the returned list will contain the |
| configuration files (app.yaml and optionally backends.yaml) found in that |
| directory. If the directory is a Java app (contains a subdirectory |
| WEB-INF with web.xml and application-web.xml files), then the returned |
| list will contain the path to the application-web.xml file, which is treated |
| as if it included web.xml. Paths that are not directories are added to the |
| returned list as is. |
| |
| Args: |
| config_paths: a list of strings that are file or directory paths. |
| |
| Returns: |
| A list of strings that are file paths. |
| """ |
| config_files = [] |
| for path in config_paths: |
| config_files += ( |
| self._config_files_from_dir(path) if os.path.isdir(path) else [path]) |
| return config_files |
| |
| def _config_files_from_dir(self, dir_path): |
| """Return a list of the configuration files found in the given directory. |
| |
| If the directory contains a subdirectory WEB-INF then we expect to find |
| web.xml and application-web.xml in that subdirectory. The returned list |
| will consist of the path to application-web.xml, which we treat as if it |
| included xml. |
| |
| Otherwise, we expect to find an app.yaml and optionally a backends.yaml, |
| and we return those in the list. |
| |
| Args: |
| dir_path: a string that is the path to a directory. |
| |
| Returns: |
| A list of strings that are file paths. |
| """ |
| web_inf = os.path.join(dir_path, 'WEB-INF') |
| if java_supported() and os.path.isdir(web_inf): |
| return self._config_files_from_web_inf_dir(web_inf) |
| app_yamls = self._files_in_dir_matching(dir_path, ['app.yaml', 'app.yml']) |
| if not app_yamls: |
| or_web_inf = ' or a WEB-INF subdirectory' if java_supported() else '' |
| raise errors.AppConfigNotFoundError( |
| '"%s" is a directory but does not contain app.yaml or app.yml%s' % |
| (dir_path, or_web_inf)) |
| backend_yamls = self._files_in_dir_matching( |
| dir_path, ['backends.yaml', 'backends.yml']) |
| return app_yamls + backend_yamls |
| |
| def _config_files_from_web_inf_dir(self, web_inf): |
| required = ['appengine-web.xml', 'web.xml'] |
| missing = [f for f in required |
| if not os.path.exists(os.path.join(web_inf, f))] |
| if missing: |
| raise errors.AppConfigNotFoundError( |
| 'The "%s" subdirectory exists but is missing %s' % |
| (web_inf, ' and '.join(missing))) |
| return [os.path.join(web_inf, required[0])] |
| |
| @staticmethod |
| def _files_in_dir_matching(dir_path, names): |
| abs_names = [os.path.join(dir_path, name) for name in names] |
| files = [f for f in abs_names if os.path.exists(f)] |
| if len(files) > 1: |
| raise errors.InvalidAppConfigError( |
| 'Directory "%s" contains %s' % (dir_path, ' and '.join(names))) |
| return files |
| |
| @property |
| def app_id(self): |
| return self._app_id |
| |
| |
| def get_app_error_file(module_configuration): |
| """Returns application specific file to handle errors. |
| |
| Dev AppServer only supports 'default' error code. |
| |
| Args: |
| module_configuration: ModuleConfiguration. |
| |
| Returns: |
| A string containing full path to error handler file or |
| None if no 'default' error handler is specified. |
| """ |
| for error_handler in module_configuration.error_handlers or []: |
| if not error_handler.error_code or error_handler.error_code == 'default': |
| return os.path.join(module_configuration.application_root, |
| error_handler.file) |
| return None |