def test_show_entry(self): """Test showing of an entry on the terminal.""" password = random_string() # Some voodoo to mock methods in classes that # have yet to be instantiated follows :-). mocked_class = type( 'TestPasswordEntry', (PasswordEntry, ), dict(text=password), ) with PatchedAttribute(qpass, 'PasswordEntry', mocked_class): with TemporaryDirectory() as directory: name = 'some/random/password' touch(os.path.join(directory, '%s.gpg' % name)) returncode, output = run_cli( main, '--password-store=%s' % directory, '--no-clipboard', name, ) assert returncode == 0 assert dedent(output) == dedent( """ {title} Password: {password} """, title=name.replace('/', ' / '), password=password, )
def create_proxy_method(name): """ Create a proxy method for use by :func:`enable_old_api()`. :param name: The name of the :class:`PseudoTerminal` method to call when the proxy method is called. :returns: A proxy method (a callable) to be installed on the :class:`CaptureOutput` class. """ # Define the proxy method. def proxy_method(self, *args, **kw): if not hasattr(self, 'output'): raise TypeError( compact(""" The old calling interface is only supported when merged=True and start_capture() has been called! """)) real_method = getattr(self.output, name) return real_method(*args, **kw) # Get the docstring of the real method. docstring = getattr(PseudoTerminal, name).__doc__ # Change the docstring to explain that this concerns a proxy method, # but only when Sphinx is active (to avoid wasting time generating a # docstring that no one is going to look at). if 'sphinx' in sys.modules: # Remove the signature from the docstring to make it possible to # remove leading indentation from the remainder of the docstring. lines = docstring.splitlines() signature = lines.pop(0) # Recompose the docstring from the signature, the remainder of the # original docstring and the note about proxy methods. docstring = '\n\n'.join([ signature, dedent('\n'.join(lines)), dedent(""" .. note:: This method is a proxy for the :func:`~PseudoTerminal.{name}()` method of the :class:`PseudoTerminal` class. It requires `merged` to be :data:`True` and it expects that :func:`start_capture()` has been called. If this is not the case then :exc:`~exceptions.TypeError` is raised. """, name=name), ]) # Copy the (possible modified) docstring. proxy_method.__doc__ = docstring return proxy_method
def test_conversion_with_system_package(self): """Convert a package and map one of its requirements to a system package.""" with TemporaryDirectory() as repository_directory: with TemporaryDirectory() as distribution_directory: # Create a temporary (and rather trivial :-) Python package. with open(os.path.join(distribution_directory, 'setup.py'), 'w') as handle: handle.write( dedent(''' from setuptools import setup setup( name='system-package-conversion-test', version='1.0', install_requires=['dbus-python'], ) ''')) # Run the conversion command. converter = self.create_isolated_converter() converter.set_repository(repository_directory) converter.use_system_package('dbus-python', fix_name_prefix('python-dbus')) archives, relationships = converter.convert( [distribution_directory]) # Make sure only one archive was generated. assert len(archives) == 1 # Use deb-pkg-tools to inspect the package metadata. metadata, contents = inspect_package(archives[0]) logger.debug("Metadata of generated package: %s", dict(metadata)) logger.debug("Contents of generated package: %s", dict(contents)) # Inspect the converted package's dependency. assert metadata['Depends'].matches(fix_name_prefix('python-dbus')), \ "py2deb failed to rewrite dependency name!"
def cryptdisks_stop_cli(): """ Usage: cryptdisks-stop-fallback NAME Reads /etc/crypttab and locks the encrypted filesystem with the given NAME. This program emulates the functionality of Debian's cryptdisks_stop program, but it only supports LUKS encryption and a small subset of the available encryption options. """ # Enable logging to the terminal and system log. coloredlogs.install(syslog=True) # Get the name of the encrypted filesystem from the command line arguments # and show a simple usage message when no name is given as an argument. try: target = sys.argv[1] except IndexError: usage(dedent(cryptdisks_stop_cli.__doc__)) else: # Call our Python implementation of `cryptdisks_stop'. try: cryptdisks_stop(target) except ValueError as e: # cryptdisks_stop() raises ValueError when the given target isn't # listed in /etc/crypttab. This doesn't deserve a traceback on the # terminal. warning("Error: %s", e) sys.exit(1) except Exception as e: # Any other exceptions are logged to the terminal and system log. logger.exception("Aborting due to exception!") sys.exit(1)
def test_install_requires_version_munging(self): """ Convert a package with a requirement whose version is "munged" by pip. Refer to :func:`py2deb.converter.PackageConverter.transform_version()` for details about the purpose of this test. """ with TemporaryDirectory() as repository_directory: with TemporaryDirectory() as distribution_directory: # Create a temporary (and rather trivial :-) Python package. with open(os.path.join(distribution_directory, 'setup.py'), 'w') as handle: handle.write(dedent(''' from setuptools import setup setup( name='install-requires-munging-test', version='1.0', install_requires=['humanfriendly==1.30.0'], ) ''')) # Run the conversion command. converter = self.create_isolated_converter() converter.set_repository(repository_directory) archives, relationships = converter.convert([distribution_directory]) # Find the generated *.deb archive. pathname = find_package_archive(archives, 'python-install-requires-munging-test') # Use deb-pkg-tools to inspect the package metadata. metadata, contents = inspect_package(pathname) logger.debug("Metadata of generated package: %s", dict(metadata)) logger.debug("Contents of generated package: %s", dict(contents)) # Inspect the converted package's dependency. assert metadata['Depends'].matches('python-humanfriendly', '1.30'), \ "py2deb failed to rewrite version of dependency!" assert not metadata['Depends'].matches('python-humanfriendly', '1.30.0'), \ "py2deb failed to rewrite version of dependency!"
def test_conversion_with_configuration_file(self): """ Convert a group of packages based on the settings in a configuration file. Repeats the same test as :func:`test_conversion_of_isolated_packages()` but instead of using command line options the conversion process is configured using a configuration file. """ # Use a temporary directory as py2deb's repository directory so that we # can easily find the *.deb archive generated by py2deb. with TemporaryDirectory() as directory: configuration_file = os.path.join(directory, 'py2deb.ini') with open(configuration_file, 'w') as handle: handle.write(dedent(''' [py2deb] repository = {repository} name-prefix = pip-accel install-prefix = /usr/lib/pip-accel auto-install = false [alternatives] /usr/bin/pip-accel = /usr/lib/pip-accel/bin/pip-accel [package:pip-accel] no-name-prefix = true [package:coloredlogs] rename = pip-accel-coloredlogs-renamed ''', repository=directory)) # Run the conversion command. exit_code, output = run_cli(main, '--config=%s' % configuration_file, 'pip-accel==0.12.6') assert exit_code == 0 # Check the results. self.check_converted_pip_accel_packages(directory)
def parse_usage(text): """ Parse a usage message by inferring its structure (and making some assumptions :-). :param text: The usage message to parse (a string). :returns: A tuple of two lists: 1. A list of strings with the paragraphs of the usage message's "introduction" (the paragraphs before the documentation of the supported command line options). 2. A list of strings with pairs of command line options and their descriptions: Item zero is a line listing a supported command line option, item one is the description of that command line option, item two is a line listing another supported command line option, etc. Usage messages in general are free format of course, however :func:`parse_usage()` assume a certain structure from usage messages in order to successfully parse them: - The usage message starts with a line ``Usage: ...`` that shows a symbolic representation of the way the program is to be invoked. - After some free form text a line ``Supported options:`` precedes the documentation of the supported command line options. - The command line options are documented as follows:: -v, --verbose Make more noise. So all of the variants of the command line option are shown together on a separate line, followed by one or more paragraphs describing the option. - There are several other minor assumptions, but to be honest I'm not sure if anyone other than me is ever going to use this functionality, so for now I won't list every intricate detail :-). If you're curious anyway, refer to the usage message of the `humanfriendly` package (defined in the :mod:`humanfriendly.cli` module) and compare it with the usage message you see when you run ``humanfriendly --help`` and the generated usage message embedded in the readme. Feel free to request more detailed documentation if you're interested in using the :mod:`humanfriendly.usage` module outside of the little ecosystem of Python packages that I have been building over the past years. """ # Split the raw usage message on lines that introduce a new command line option. chunks = [dedent(c) for c in re.split(OPTION_PATTERN, text) if c and not c.isspace()] # Split the introduction (the text before any options) into one or more paragraphs. introduction = [join_lines(p) for p in split_paragraphs(chunks.pop(0))] # Should someone need to easily debug the parsing performed here. logger.debug("Parsed introduction: %s", introduction) logger.debug("Parsed options: %s", chunks) return introduction, chunks
def test_format_text(self): """Test human friendly formatting of password store entries.""" entry = PasswordEntry(name='some/random/password', store=object()) set_property(entry, 'text', random_string()) self.assertEquals( # We enable ANSI escape sequences but strip them before we # compare the generated string. This may seem rather pointless # but it ensures that the relevant code paths are covered :-). dedent( ansi_strip( entry.format_text(include_password=True, use_colors=True))), dedent(''' some / random / password Password: {value} ''', value=entry.text))
def create_proxy_method(name): """ Create a proxy method for use by :func:`enable_old_api()`. :param name: The name of the :class:`PseudoTerminal` method to call when the proxy method is called. :returns: A proxy method (a callable) to be installed on the :class:`CaptureOutput` class. """ # Define the proxy method. def proxy_method(self, *args, **kw): if not hasattr(self, 'output'): raise TypeError(compact(""" The old calling interface is only supported when merged=True and start_capture() has been called! """)) real_method = getattr(self.output, name) return real_method(*args, **kw) # Get the docstring of the real method. docstring = getattr(PseudoTerminal, name).__doc__ # Change the docstring to explain that this concerns a proxy method, # but only when Sphinx is active (to avoid wasting time generating a # docstring that no one is going to look at). if 'sphinx' in sys.modules: # Remove the signature from the docstring to make it possible to # remove leading indentation from the remainder of the docstring. lines = docstring.splitlines() signature = lines.pop(0) # Recompose the docstring from the signature, the remainder of the # original docstring and the note about proxy methods. docstring = '\n\n'.join([ signature, dedent('\n'.join(lines)), dedent(""" .. note:: This method is a proxy for the :func:`~PseudoTerminal.{name}()` method of the :class:`PseudoTerminal` class. It requires `merged` to be :data:`True` and it expects that :func:`start_capture()` has been called. If this is not the case then :exc:`~exceptions.TypeError` is raised. """, name=name), ]) # Copy the (possible modified) docstring. proxy_method.__doc__ = docstring return proxy_method
def normalize_repr_output(expression): """ Enable string comparison between :func:`repr()` output on different Python versions. This function enables string comparison between :func:`repr()` output on Python 2 (where Unicode strings have the ``u`` prefix) and Python 3 (where Unicode strings are the default and no prefix is emitted by :func:`repr()`). """ return re.sub(r'\bu([\'"])', r'\1', dedent(expression).strip())
def mock_ip(self): """Mocked ``ip route show`` program.""" return MockedProgram(name='ip', script=dedent(""" cat << EOF default via 192.168.1.1 dev wlp3s0 proto dhcp metric 600 169.254.0.0/16 dev virbr0 scope link metric 1000 linkdown 192.168.0.0/16 dev wlp3s0 proto kernel scope link src 192.168.2.214 metric 600 192.168.122.0/24 dev virbr0 proto kernel scope link src 192.168.122.1 linkdown EOF """))
def deprecation_note_callback(app, what, name, obj, options, lines): """ Automatically document aliases defined using :func:`~humanfriendly.deprecation.define_aliases()`. Refer to :func:`enable_deprecation_notes()` to enable the use of this function (you probably don't want to call :func:`deprecation_note_callback()` directly). This function implements a callback for ``autodoc-process-docstring`` that reformats module docstrings to append an overview of aliases defined by the module. 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 isinstance(obj, types.ModuleType) and lines: aliases = get_aliases(obj.__name__) if aliases: # Convert the existing docstring to a string and remove leading # indentation from that string, otherwise our generated content # would have to match the existing indentation in order not to # break docstring parsing (because indentation is significant # in the reStructuredText format). blocks = [dedent("\n".join(lines))] # Use an admonition to group the deprecated aliases together and # to distinguish them from the autodoc entries that follow. blocks.append(".. note:: Deprecated names") indent = " " * 3 if len(aliases) == 1: explanation = """ The following alias exists to preserve backwards compatibility, however a :exc:`~exceptions.DeprecationWarning` is triggered when it is accessed, because this alias will be removed in a future release. """ else: explanation = """ The following aliases exist to preserve backwards compatibility, however a :exc:`~exceptions.DeprecationWarning` is triggered when they are accessed, because these aliases will be removed in a future release. """ blocks.append(indent + compact(explanation)) for name, target in aliases.items(): blocks.append(format("%s.. data:: %s", indent, name)) blocks.append( format("%sAlias for :obj:`%s`.", indent * 2, target)) update_lines(lines, "\n\n".join(blocks))
def install_sources_file(self): """Install a 'package resource list' that points ``apt`` to the NodeSource repository.""" logger.info("Installing package resource list (%s) ..", self.sources_file) sources_list = dedent(''' # {filename}: # Get NodeJS binaries from the NodeSource repository. deb https://deb.nodesource.com/{version} {codename} main deb-src https://deb.nodesource.com/{version} {codename} main ''', filename=self.sources_file, version=self.nodejs_version, codename=self.distribution_codename) # TODO It would be nicer if context.write_file() accepted sudo=True! self.context.execute('cat > %s' % quote(self.sources_file), input=sources_list, sudo=True)
def mock_arp(self): """Mocked ``arp`` program.""" return MockedProgram(name='arp', script=dedent(""" cat << EOF Address HWtype HWaddress Flags Mask Iface 192.168.1.4 ether 4b:21:f5:49:88:85 C wlp3s0 192.168.3.28 ether 3d:a6:19:62:9a:83 C wlp3s0 192.168.3.5 ether c5:4c:8d:56:25:0c C wlp3s0 192.168.1.1 ether 80:34:58:ad:6c:f5 C wlp3s0 192.168.3.2 ether 20:22:a0:22:0c:db C wlp3s0 192.168.1.12 ether ad:12:75:46:e9:70 C wlp3s0 192.168.3.6 ether 08:33:c7:ef:f7:27 C wlp3s0 192.168.1.11 ether c9:0e:95:24:68:31 C wlp3s0 192.168.3.4 ether e7:e6:2c:3b:bc:8a C wlp3s0 192.168.3.3 ether 72:d7:d3:2c:54:93 C wlp3s0 192.168.1.6 ether 95:ef:85:cf:d3:36 C wlp3s0 192.168.3.7 ether 65:c0:be:40:cd:31 C wlp3s0 EOF """))
def glob(self, pattern): """ Find matches for a given filename pattern. :param pattern: A filename pattern (a string). :returns: A list of strings with matches. Some implementation notes: - This method *emulates* filename globbing as supported by system shells like Bash and ZSH. It works by forking a Python interpreter and using that to call the :func:`glob.glob()` function. This approach is of course rather heavyweight. - Initially this method used Bash for filename matching (similar to `this StackOverflow answer <https://unix.stackexchange.com/a/34012/44309>`_) but I found it impossible to make this work well for patterns containing whitespace. - I took the whitespace issue as a sign that I was heading down the wrong path (trying to add robustness to a fragile solution) and so the new implementation was born (which prioritizes robustness over performance). """ listing = self.capture( 'python', input=dedent( r''' import glob matches = glob.glob({pattern}) print('\x00'.join(matches)) ''', pattern=repr(pattern), ), ) return split(listing, '\x00')
def parse_usage(text): """ Parse a usage message by inferring its structure (and making some assumptions :-). :param text: The usage message to parse (a string). :returns: A tuple of two lists: 1. A list of strings with the paragraphs of the usage message's "introduction" (the paragraphs before the documentation of the supported command line options). 2. A list of strings with pairs of command line options and their descriptions: Item zero is a line listing a supported command line option, item one is the description of that command line option, item two is a line listing another supported command line option, etc. Usage messages in general are free format of course, however :func:`parse_usage()` assume a certain structure from usage messages in order to successfully parse them: - The usage message starts with a line ``Usage: ...`` that shows a symbolic representation of the way the program is to be invoked. - After some free form text a line ``Supported options:`` (surrounded by empty lines) precedes the documentation of the supported command line options. - The command line options are documented as follows:: -v, --verbose Make more noise. So all of the variants of the command line option are shown together on a separate line, followed by one or more paragraphs describing the option. - There are several other minor assumptions, but to be honest I'm not sure if anyone other than me is ever going to use this functionality, so for now I won't list every intricate detail :-). If you're curious anyway, refer to the usage message of the `humanfriendly` package (defined in the :mod:`humanfriendly.cli` module) and compare it with the usage message you see when you run ``humanfriendly --help`` and the generated usage message embedded in the readme. Feel free to request more detailed documentation if you're interested in using the :mod:`humanfriendly.usage` module outside of the little ecosystem of Python packages that I have been building over the past years. """ introduction = [] documented_options = [] # Split the raw usage message into paragraphs. paragraphs = split_paragraphs(text) # Get the paragraphs that are part of the introduction. while paragraphs: # Check whether we've found the end of the introduction. end_of_intro = (paragraphs[0] == START_OF_OPTIONS_MARKER) # Append the current paragraph to the introduction. introduction.append(join_lines(paragraphs.pop(0))) # Stop after we've processed the complete introduction. if end_of_intro: break logger.debug("Parsed introduction: %s", introduction) # Parse the paragraphs that document command line options. while paragraphs: documented_options.append(dedent(paragraphs.pop(0))) description = [] while paragraphs: # Check if the next paragraph starts the documentation of another # command line option. if all(OPTION_PATTERN.match(t) for t in split(paragraphs[0])): break else: description.append(paragraphs.pop(0)) # Join the description's paragraphs back together so we can remove # common leading indentation. documented_options.append(dedent('\n\n'.join(description))) logger.debug("Parsed options: %s", documented_options) return introduction, documented_options
def parse_usage(text): """ Parse a usage message by inferring its structure (and making some assumptions :-). :param text: The usage message to parse (a string). :returns: A tuple of two lists: 1. A list of strings with the paragraphs of the usage message's "introduction" (the paragraphs before the documentation of the supported command line options). 2. A list of strings with pairs of command line options and their descriptions: Item zero is a line listing a supported command line option, item one is the description of that command line option, item two is a line listing another supported command line option, etc. Usage messages in general are free format of course, however :func:`parse_usage()` assume a certain structure from usage messages in order to successfully parse them: - The usage message starts with a line ``Usage: ...`` that shows a symbolic representation of the way the program is to be invoked. - After some free form text a line ``Supported options:`` (surrounded by empty lines) precedes the documentation of the supported command line options. - The command line options are documented as follows:: -v, --verbose Make more noise. So all of the variants of the command line option are shown together on a separate line, followed by one or more paragraphs describing the option. - There are several other minor assumptions, but to be honest I'm not sure if anyone other than me is ever going to use this functionality, so for now I won't list every intricate detail :-). If you're curious anyway, refer to the usage message of the `humanfriendly` package (defined in the :mod:`humanfriendly.cli` module) and compare it with the usage message you see when you run ``humanfriendly --help`` and the generated usage message embedded in the readme. Feel free to request more detailed documentation if you're interested in using the :mod:`humanfriendly.usage` module outside of the little ecosystem of Python packages that I have been building over the past years. """ introduction = [] documented_options = [] # Split the raw usage message into paragraphs. paragraphs = split_paragraphs(text) # Get the paragraphs that are part of the introduction. while paragraphs: # Check whether we've found the end of the introduction. end_of_intro = (paragraphs[0] == START_OF_OPTIONS_MARKER) # Append the current paragraph to the introduction. introduction.append(paragraphs.pop(0)) # Stop after we've processed the complete introduction. if end_of_intro: break logger.debug("Parsed introduction: %s", introduction) # Parse the paragraphs that document command line options. while paragraphs: documented_options.append(dedent(paragraphs.pop(0))) description = [] while paragraphs: # Check if the next paragraph starts the documentation of another # command line option. We split on a comma followed by a space so # that our parsing doesn't trip up when the label used for an # option's value contains commas. tokens = [ t.strip() for t in re.split(r',\s', paragraphs[0]) if t and not t.isspace() ] if all(OPTION_PATTERN.match(t) for t in tokens): break else: description.append(paragraphs.pop(0)) # Join the description's paragraphs back together so we can remove # common leading indentation. documented_options.append(dedent('\n\n'.join(description))) logger.debug("Parsed options: %s", documented_options) return introduction, documented_options