def append_property_docs(app, what, name, obj, options, lines): """ Render an overview with properties and methods of :class:`.PropertyManager` subclasses. This function implements a callback for ``autodoc-process-docstring`` that generates and appends an overview of member details to the docstrings of :class:`.PropertyManager` subclasses. The parameters expected by this function are those defined for Sphinx event callback functions (i.e. I'm not going to document them here :-). """ if is_suitable_type(obj): paragraphs = [] details = TypeInspector(type=obj) hints = (details.required_hint, details.initializer_hint) if any(hints): paragraphs.append(' '.join(h for h in hints if h)) paragraphs.append( format("Here's an overview of the :class:`%s` class:", obj.__name__)) # Whitespace in labels is replaced with non breaking spaces to disable wrapping of the label text. data = [(format("%s:", label.replace(' ', u'\u00A0')), text) for label, text in details.overview if text] paragraphs.append(format_rst_table(data)) if lines: lines.append('') lines.extend('\n\n'.join(paragraphs).splitlines())
def compose_usage_notes(self): """ Get a description of the property's semantics to include in its documentation. :returns: A list of strings describing the semantics of the :class:`custom_property` in reStructuredText_ format with Sphinx_ directives. .. _reStructuredText: https://en.wikipedia.org/wiki/ReStructuredText .. _Sphinx: http://sphinx-doc.org/ """ template = DYNAMIC_PROPERTY_NOTE if self.dynamic else CUSTOM_PROPERTY_NOTE cls = custom_property if self.dynamic else self.__class__ dotted_path = "%s.%s" % (cls.__module__, cls.__name__) notes = [format(template, name=self.__name__, type=dotted_path)] if self.environment_variable: notes.append(format(ENVIRONMENT_PROPERTY_NOTE, variable=self.environment_variable)) if self.required: notes.append(format(REQUIRED_PROPERTY_NOTE, name=self.__name__)) if self.key: notes.append(KEY_PROPERTY_NOTE) if self.writable: notes.append(WRITABLE_PROPERTY_NOTE) if self.cached: notes.append(CACHED_PROPERTY_NOTE) if self.resettable: if self.cached: notes.append(RESETTABLE_CACHED_PROPERTY_NOTE) else: notes.append(RESETTABLE_WRITABLE_PROPERTY_NOTE) return notes
def compose_usage_notes(self): """ Get a description of the property's semantics to include in its documentation. :returns: A list of strings describing the semantics of the :class:`custom_property` in reStructuredText_ format with Sphinx_ directives. .. _reStructuredText: https://en.wikipedia.org/wiki/ReStructuredText .. _Sphinx: http://sphinx-doc.org/ """ template = DYNAMIC_PROPERTY_NOTE if self.dynamic else CUSTOM_PROPERTY_NOTE cls = custom_property if self.dynamic else self.__class__ dotted_path = "%s.%s" % (cls.__module__, cls.__name__) notes = [format(template, name=self.__name__, type=dotted_path)] if self.environment_variable: notes.append( format(ENVIRONMENT_PROPERTY_NOTE, variable=self.environment_variable)) if self.required: notes.append(format(REQUIRED_PROPERTY_NOTE, name=self.__name__)) if self.key: notes.append(KEY_PROPERTY_NOTE) if self.writable: notes.append(WRITABLE_PROPERTY_NOTE) if self.cached: notes.append(CACHED_PROPERTY_NOTE) if self.resettable: if self.cached: notes.append(RESETTABLE_CACHED_PROPERTY_NOTE) else: notes.append(RESETTABLE_WRITABLE_PROPERTY_NOTE) return notes
def error_message(self): """A user friendly explanation of how the remote command failed (a string or :data:`None`).""" if self.error_type is RemoteConnectFailed: return format("SSH connection to %s failed! (SSH command: %s)", self.ssh_alias, quote(self.command_line)) elif self.error_type is RemoteCommandNotFound: return format("External command on %s isn't available! (SSH command: %s)", self.ssh_alias, quote(self.command_line)) elif self.error_type is RemoteCommandFailed: return format("External command on %s failed with exit code %s! (SSH command: %s)", self.ssh_alias, self.returncode, quote(self.command_line))
def error_message(self): """A user friendly explanation of how the remote command failed (a string or :data:`None`).""" if self.error_type is RemoteConnectFailed: return format("SSH connection to %s failed! (SSH command: %s)", self.ssh_alias, quote(self.command_line)) elif self.error_type is RemoteCommandNotFound: return format( "External command on %s isn't available! (SSH command: %s)", self.ssh_alias, quote(self.command_line)) elif self.error_type is RemoteCommandFailed: return format( "External command on %s failed with exit code %s! (SSH command: %s)", self.ssh_alias, self.returncode, quote(self.command_line))
def check_usage_notes(self): """"Check whether the correct notes are embedded in the documentation.""" class DocumentationTest(object): @self.property_type def documented_property(self): """Documentation written by the author.""" return random.random() documentation = DocumentationTest.documented_property.__doc__ # Test that the sentence added for custom properties is always present. cls = custom_property if self.property_type.dynamic else self.property_type custom_property_note = format( DYNAMIC_PROPERTY_NOTE if self.property_type.dynamic else CUSTOM_PROPERTY_NOTE, name='documented_property', type="%s.%s" % (cls.__module__, cls.__name__), ) if DocumentationTest.documented_property.usage_notes: assert custom_property_note in documentation else: assert custom_property_note not in documentation # If CUSTOM_PROPERTY_NOTE is not present we assume that none of the # other usage notes will be present either. return # Test that the sentence added for writable properties is present when applicable. assert self.property_type.writable == (WRITABLE_PROPERTY_NOTE in documentation) # Test that the sentence added for cached properties is present when applicable. assert self.property_type.cached == (CACHED_PROPERTY_NOTE in documentation) # Test that the sentence added for resettable properties is present when applicable. if self.is_resettable: assert self.is_cached == (RESETTABLE_CACHED_PROPERTY_NOTE in documentation) assert self.is_writable == (RESETTABLE_WRITABLE_PROPERTY_NOTE in documentation) else: assert RESETTABLE_CACHED_PROPERTY_NOTE not in documentation assert RESETTABLE_WRITABLE_PROPERTY_NOTE not in documentation # Test that the sentence added for required properties is present when applicable. required_property_note = format(REQUIRED_PROPERTY_NOTE, name='documented_property') assert self.property_type.required == (required_property_note in documentation) # Test that the sentence added for environment properties is present when applicable. environment_note = format( ENVIRONMENT_PROPERTY_NOTE, variable=self.property_type.environment_variable) assert bool( self.property_type.environment_variable) == (environment_note in documentation)
def error_message(self): """An error message that explains which commands *failed unexpectedly* (a string).""" summary = format("%i out of %s failed unexpectedly:", self.pool.num_failed, pluralize(self.pool.num_commands, "command")) details = "\n".join(" - %s" % cmd.error_message for cmd in self.commands) return summary + "\n\n" + details
def run(self): """ Keep spawning commands and collecting results until all commands have run. :returns: The value of :attr:`results`. :raises: Any exceptions raised by :func:`collect()`. This method calls :func:`spawn()` and :func:`collect()` in a loop until all commands registered using :func:`add()` have run and finished. If :func:`collect()` raises an exception any running commands are terminated before the exception is propagated to the caller. If you're writing code where you want to own the main loop then consider calling :func:`spawn()` and :func:`collect()` directly instead of using :func:`run()`. When :attr:`concurrency` is set to one, specific care is taken to make sure that the callbacks configured by :attr:`.start_event` and :attr:`.finish_event` are called in the expected (intuitive) order. """ # Start spawning processes to execute the commands. timer = Timer() logger.debug("Preparing to run %s with a concurrency of %i ..", pluralize(self.num_commands, "command"), self.concurrency) try: with Spinner(interactive=self.spinner, timer=timer) as spinner: num_started = 0 num_collected = 0 while not self.is_finished: # When concurrency is set to one (I know, initially it # sounds like a silly use case, bear with me) I want the # start_event and finish_event callbacks of external # commands to fire in the right order. The following # conditional is intended to accomplish this goal. if self.concurrency > (num_started - num_collected): num_started += self.spawn() num_collected += self.collect() spinner.step(label=format( "Waiting for %i/%i %s", self.num_commands - self.num_finished, self.num_commands, "command" if self.num_commands == 1 else "commands", )) spinner.sleep() except Exception: if self.num_running > 0: logger.warning( "Command pool raised exception, terminating running commands!" ) # Terminate commands that are still running. self.terminate() # Re-raise the exception to the caller. raise # Collect the output and return code of any commands not yet collected. self.collect() logger.debug("Finished running %s in %s.", pluralize(self.num_commands, "command"), timer) # Report the results to the caller. return self.results
def run(self): """ Keep spawning commands and collecting results until all commands have run. :returns: The value of :attr:`results`. :raises: Any exceptions raised by :func:`collect()`. This method calls :func:`spawn()` and :func:`collect()` in a loop until all commands registered using :func:`add()` have run and finished. If :func:`collect()` raises an exception any running commands are terminated before the exception is propagated to the caller. If you're writing code where you want to own the main loop then consider calling :func:`spawn()` and :func:`collect()` directly instead of using :func:`run()`. When :attr:`concurrency` is set to one, specific care is taken to make sure that the callbacks configured by :attr:`.start_event` and :attr:`.finish_event` are called in the expected (intuitive) order. """ # Start spawning processes to execute the commands. timer = Timer() logger.debug("Preparing to run %s with a concurrency of %i ..", pluralize(self.num_commands, "command"), self.concurrency) try: with Spinner(interactive=self.spinner, timer=timer) as spinner: num_started = 0 num_collected = 0 while not self.is_finished: # When concurrency is set to one (I know, initially it # sounds like a silly use case, bear with me) I want the # start_event and finish_event callbacks of external # commands to fire in the right order. The following # conditional is intended to accomplish this goal. if self.concurrency > (num_started - num_collected): num_started += self.spawn() num_collected += self.collect() spinner.step(label=format( "Waiting for %i/%i %s", self.num_commands - self.num_finished, self.num_commands, "command" if self.num_commands == 1 else "commands", )) spinner.sleep() except Exception: if self.num_running > 0: logger.warning("Command pool raised exception, terminating running commands!") # Terminate commands that are still running. self.terminate() # Re-raise the exception to the caller. raise # Collect the output and return code of any commands not yet collected. self.collect() logger.debug("Finished running %s in %s.", pluralize(self.num_commands, "command"), timer) # Report the results to the caller. return self.results
def overview(self): """Render an overview with related members grouped together.""" return ( ("Superclass" if len(self.type.__bases__) == 1 else "Superclasses", concatenate( format(":class:`~%s.%s`", b.__module__, b.__name__) for b in self.type.__bases__)), ("Special methods", self.format_methods(self.special_methods)), ("Public methods", self.format_methods(self.public_methods)), ("Properties", self.format_properties(n for n, v in self.properties)), )
def __getitem__(self, name): """ Get the objects associated to the given tag. :param name: The name of the tag (a string). :returns: A :class:`set` of objects associated to the tag. :raises: :exc:`.EmptyTagError` when no associated objects are available. """ objects = self.tags[name].objects if not objects: msg = "The tag '%s' doesn't match anything!" raise EmptyTagError(format(msg, name)) return objects
def generate_sources_list(mirror_url, codename, suites=DEFAULT_SUITES, components=VALID_COMPONENTS, enable_sources=False): """ Generate the contents of ``/etc/apt/sources.list`` for a Debian system. :param mirror_url: The base URL of the mirror (a string). :param codename: The codename of a Debian release (a string like 'wheezy' or 'jessie') or a Debian release class (a string like 'stable', 'testing', etc). :param suites: An iterable of strings (defaults to :data:`DEFAULT_SUITES`, refer to :data:`VALID_SUITES` for details). :param components: An iterable of strings (refer to :data:`VALID_COMPONENTS` for details). :param enable_sources: :data:`True` to include ``deb-src`` entries, :data:`False` to omit them. :returns: The suggested contents of ``/etc/apt/sources.list`` (a string). """ # Validate the suites. invalid_suites = [s for s in suites if s not in VALID_SUITES] if invalid_suites: msg = "Invalid Debian suite(s) given! (%s)" raise ValueError(msg % invalid_suites) # Validate the components. invalid_components = [c for c in components if c not in VALID_COMPONENTS] if invalid_components: msg = "Invalid Debian component(s) given! (%s)" raise ValueError(msg % invalid_components) # Generate the /etc/apt/sources.list file contents. lines = [] directives = ('deb', 'deb-src') if enable_sources else ('deb', ) for suite in suites: for directive in directives: lines.append( format( '{directive} {mirror} {suite} {components}', directive=directive, mirror=( OLD_RELEASES_URL if mirrors_are_equal( mirror_url, OLD_RELEASES_URL) else (SECURITY_URL if suite == 'security' else mirror_url)), suite=(codename if suite == 'release' else (('%s/updates' % codename if suite == 'security' else codename + '-' + suite))), components=' '.join(components), )) return '\n'.join(lines)
def check_usage_notes(self): """"Check whether the correct notes are embedded in the documentation.""" class DocumentationTest(object): @self.property_type def documented_property(self): """Documentation written by the author.""" return random.random() documentation = DocumentationTest.documented_property.__doc__ # Test that the sentence added for custom properties is always present. cls = custom_property if self.property_type.dynamic else self.property_type custom_property_note = format( DYNAMIC_PROPERTY_NOTE if self.property_type.dynamic else CUSTOM_PROPERTY_NOTE, name='documented_property', type="%s.%s" % (cls.__module__, cls.__name__), ) if DocumentationTest.documented_property.usage_notes: assert custom_property_note in documentation else: assert custom_property_note not in documentation # If CUSTOM_PROPERTY_NOTE is not present we assume that none of the # other usage notes will be present either. return # Test that the sentence added for writable properties is present when applicable. assert self.property_type.writable == (WRITABLE_PROPERTY_NOTE in documentation) # Test that the sentence added for cached properties is present when applicable. assert self.property_type.cached == (CACHED_PROPERTY_NOTE in documentation) # Test that the sentence added for resettable properties is present when applicable. if self.is_resettable: assert self.is_cached == (RESETTABLE_CACHED_PROPERTY_NOTE in documentation) assert self.is_writable == (RESETTABLE_WRITABLE_PROPERTY_NOTE in documentation) else: assert RESETTABLE_CACHED_PROPERTY_NOTE not in documentation assert RESETTABLE_WRITABLE_PROPERTY_NOTE not in documentation # Test that the sentence added for required properties is present when applicable. required_property_note = format(REQUIRED_PROPERTY_NOTE, name='documented_property') assert self.property_type.required == (required_property_note in documentation) # Test that the sentence added for environment properties is present when applicable. environment_note = format(ENVIRONMENT_PROPERTY_NOTE, variable=self.property_type.environment_variable) assert bool(self.property_type.environment_variable) == (environment_note in documentation)
def __init__(self, command, timeout): """ Initialize a :class:`CommandTimedOut` object. :param command: The command that timed out (an :class:`~executor.ExternalCommand` object). :param timeout: The timeout that was exceeded (a number). """ super(CommandTimedOut, self).__init__( command=command, error_message=format( "External command exceeded timeout of %s: %s", format_timespan(timeout), quote(command.command_line), ), )
def search_messages(self, keywords): """Search the chat messages in the local archive for the given keyword(s).""" query = (self.session.query(Message).join(Conversation).join( Account).outerjoin( (Contact, Contact.id == Message.sender_id)).outerjoin( (EmailAddress, Contact.email_addresses))) for kw in keywords: search_term = format(u"%{kw}%", kw=kw) query = query.filter( Account.backend.like(search_term) | Account.name.like(search_term) | Conversation.name.like(search_term) | Contact.full_name.like(search_term) | EmailAddress.value.like(search_term) | Message.timestamp.like(search_term) | Message.text.like(search_term)) return query.order_by(Message.timestamp)
def wait_until_connected(self): """ Wait until connections are being accepted. :raises: :exc:`TimeoutError` when the SSH server isn't fast enough to initialize. """ timer = Timer() with Spinner(timer=timer) as spinner: while not self.is_connected: if timer.elapsed_time > self.wait_timeout: raise TimeoutError(format( "Failed to establish connection to %s within configured timeout of %s!", self, format_timespan(self.wait_timeout), )) spinner.step(label="Waiting for %s to accept connections" % self) spinner.sleep() logger.debug("Waited %s for %s to accept connections.", timer, self)
def get_secret_from_store(name, directory=None): """ Use :mod:`qpass` to get a secret from ``~/.password-store``. :param name: The name of a password or a search pattern that matches a single entry in the password store (a string). :param directory: The directory to use (a string, defaults to ``~/.password-store``). :returns: The secret (a string). :raises: :exc:`exceptions.ValueError` when the given `name` doesn't match any entries or matches multiple entries in the password store. """ kw = dict(directory=directory) if directory else {} store = PasswordStore(**kw) matches = store.smart_search(name) if len(matches) != 1: msg = "Expected exactly one match in password database! (input: %s)" raise ValueError(format(msg, name)) return matches[0].password
def parse(self, value): """ Parse a string expression into a :class:`Tag` object. :param value: The tag expression to parse (a string). :returns: A :class:`Tag` object. :raises: :exc:`~exceptions.ValueError` for unsupported `value` types. During normal use you won't need the :func:`parse()` method, in fact it's not currently being used anywhere in :mod:`gentag`. This method was originally created with the idea of having :func:`define()` parse string expressions up front to validate their syntax, however this approach has since been abandoned. The :func:`parse()` method now remains because it may be useful to callers for unforeseen use cases. """ if isinstance(value, string_types): # We override __builtins__ to avoid leaking any built-ins into eval(). return eval(value, dict(__builtins__={}), self.tags) else: msg = "Unsupported value type! (%r)" raise ValueError(format(msg, value))
def evaluate_raw(self, expression): """ Get the objects matching the given expression. :param expression: The tag expression to evaluate (a string). :returns: A :class:`set` with matching objects. :raises: :exc:`.TagExpressionError` when the given expression cannot be evaluated due to a syntax error. This method uses :func:`eval()` to evaluate the expression given by the caller, however it overrides ``__builtins__`` to avoid leaking any built-ins into the :func:`eval()` call. """ try: logger.debug("Evaluating expression '%s' ..", expression) objects = eval(expression, dict(__builtins__={}), self.objects) logger.debug("The expression matched %s.", pluralize(len(objects), "object")) return objects except SyntaxError as e: msg = "Failed to evaluate tag expression due to syntax error! (%s)" raise TagExpressionError(format(msg, e))
def wait_until_connected(self): """ Wait until connections are being accepted. :raises: :exc:`TimeoutError` when the SSH server isn't fast enough to initialize. """ timer = Timer() with Spinner(timer=timer) as spinner: while not self.is_connected: if timer.elapsed_time > self.wait_timeout: raise TimeoutError( format( "Failed to establish connection to %s within configured timeout of %s!", self, format_timespan(self.wait_timeout), )) spinner.step(label="Waiting for %s to accept connections" % self) spinner.sleep() logger.debug("Waited %s for %s to accept connections.", timer, self)
def generate_sources_list(mirror_url, codename, suites=DEFAULT_SUITES, components=VALID_COMPONENTS, enable_sources=False): """ Generate the contents of ``/etc/apt/sources.list`` for an Ubuntu system. :param mirror_url: The base URL of the mirror (a string). :param codename: The codename of the Ubuntu release (a string like 'trusty' or 'xenial'). :param suites: An iterable of strings (defaults to :data:`DEFAULT_SUITES`, refer to :data:`VALID_SUITES` for details). :param components: An iterable of strings (refer to :data:`VALID_COMPONENTS` for details). :param enable_sources: :data:`True` to include ``deb-src`` entries, :data:`False` to omit them. :returns: The suggested contents of ``/etc/apt/sources.list`` (a string). """ # Validate the suites. invalid_suites = [s for s in suites if s not in VALID_SUITES] if invalid_suites: msg = "Invalid Ubuntu suite(s) given! (%s)" raise ValueError(msg % invalid_suites) # Validate the components. invalid_components = [c for c in components if c not in VALID_COMPONENTS] if invalid_components: msg = "Invalid Ubuntu component(s) given! (%s)" raise ValueError(msg % invalid_components) # Generate the /etc/apt/sources.list file contents. lines = [] directives = ('deb', 'deb-src') if enable_sources else ('deb',) for suite in suites: for directive in directives: lines.append(format( '{directive} {mirror} {suite} {components}', directive=directive, mirror=(OLD_RELEASES_URL if mirrors_are_equal(mirror_url, OLD_RELEASES_URL) else (SECURITY_URL if suite == 'security' else mirror_url)), suite=(codename if suite == 'release' else codename + '-' + suite), components=' '.join(components), )) return '\n'.join(lines)
def documentation(self): r""" Configuration documentation in reStructuredText_ syntax (a string). The purpose of the :attr:`documentation` property is to provide documentation on the integration of :class:`ConfigLoader` into other projects without denormalizing the required knowledge via copy/paste. .. _reStructuredText: https://en.wikipedia.org/wiki/ReStructuredText """ from humanfriendly.tables import format_rst_table formatted_table = format_rst_table([ (directory, self.get_main_pattern(directory).replace('*', r'\*'), self.get_modular_pattern(directory).replace('*', r'\*')) for directory in self.base_directories ], [ "Directory", "Main configuration file", "Modular configuration files", ]) return format(DOCUMENTATION_TEMPLATE, table=formatted_table).strip()
def confirm_installation(self, requirement, missing_dependencies, install_command): """ Ask the operator's permission to install missing system packages. :param requirement: A :class:`.Requirement` object. :param missing_dependencies: A list of strings with missing dependencies. :param install_command: A list of strings with the command line needed to install the missing dependencies. :raises: :exc:`.DependencyInstallationRefused` when the operator refuses. """ try: return prompt_for_confirmation( format( "Do you want me to install %s %s?", "this" if len(missing_dependencies) == 1 else "these", "dependency" if len(missing_dependencies) == 1 else "dependencies", ), default=True, ) except KeyboardInterrupt: # Control-C is a negative response but doesn't # otherwise interrupt the program flow. return False
def confirm_installation(self, requirement, missing_dependencies, install_command): """ Ask the operator's permission to install missing system packages. :param requirement: A :class:`.Requirement` object. :param missing_dependencies: A list of strings with missing dependencies. :param install_command: A list of strings with the command line needed to install the missing dependencies. :raises: :exc:`.DependencyInstallationRefused` when the operator refuses. """ try: return prompt_for_confirmation(format( "Do you want me to install %s %s?", "this" if len(missing_dependencies) == 1 else "these", "dependency" if len(missing_dependencies) == 1 else "dependencies", ), default=True) except KeyboardInterrupt: # Control-C is a negative response but doesn't # otherwise interrupt the program flow. return False
def define(self, name, value): """ Define the value of a tag. :param name: The name of the tag (a string). :param value: A string containing an expression or an iterable of values associated to the given tag. :returns: The :class:`Tag` object. :raises: :exc:`~exceptions.ValueError` for unsupported `value` types. """ if isinstance(value, string_types): logger.debug("Setting expression of tag '%s' to: %s", name, value) tag = self.tags[name] tag.expression = value return tag elif isinstance(value, collections.Iterable): logger.debug("Setting objects of tag '%s' to: %s", name, value) tag = self.tags[name] tag.objects = value return tag else: msg = "Unsupported value for tag '%s'! (%r)" raise ValueError(format(msg, name, value))
def get_modular_pattern(self, directory): """ Get the :func:`~glob.glob()` pattern to find modular configuration files. :param directory: The pathname of a base directory (a string). :returns: A filename pattern (a string). This method generates a pattern that matches a directory whose name is based on :attr:`program_name` with the suffix ``.d`` containing files matching the configured :attr:`filename_extension`. Here's an example: >>> from update_dotdee import ConfigLoader >>> loader = ConfigLoader(program_name='update-dotdee') >>> [loader.get_modular_pattern(d) for d in loader.base_directories] ['/etc/update-dotdee.d/*.ini', '~/.update-dotdee.d/*.ini', '~/.config/update-dotdee.d/*.ini'] """ return os.path.join(directory, format( '{prefix}{program_name}.d/*.{extension}', extension=self.filename_extension.lstrip('.'), program_name=self.program_name, prefix=self.get_prefix(directory), ))
def format_methods(self, names): """Format a list of method names as reStructuredText.""" return concatenate(format(":func:`%s()`", n) for n in sorted(names))
def format_properties(self, names): """Format a list of property names as reStructuredText.""" return concatenate(format(":attr:`%s`", n) for n in sorted(names))
def check_response(self, response, message, *args, **kw): """Validate an IMAP server response.""" logger.debug("IMAP response: rv=%r, data=%r", response[0], response[1]) if response[0] != "OK": raise Exception(format(message, *args, **kw)) return response[1]
def __str__(self): """Render a human friendly representation.""" return format("%s://%s:%i", self.scheme, self.hostname, self.port_number)
def report_issue(self, message, *args, **kw): """Handle a problem by raising an exception or logging a warning (depending on :attr:`strict`).""" if self.strict: raise ValueError(format(message, *args, **kw)) else: logger.warning(format(message, *args, **kw))
def render_location(self, scheme=None, hostname=None, port_number=None): """Render a human friendly representation of an :class:`EphemeralTCPServer` object.""" return format("{scheme}://{host}:{port}", scheme=scheme or self.scheme, host=hostname or self.hostname, port=port_number or self.port_number)