Reorganized hscommon documentation

Removed hscommon's "docs" folder and moved all documentation directly
into docstrings. Then, in dupeGuru's developer documentation, added
autodoc references to relevant modules.

The result is a much more usable hscommon documentation.
This commit is contained in:
Virgil Dupras 2013-11-16 14:46:34 -05:00
parent 10dbfa9b38
commit 508e9a5d94
27 changed files with 224 additions and 574 deletions

View File

@ -0,0 +1,12 @@
core
====
.. toctree::
:maxdepth: 2
app
fs
engine
directories
results
gui

View File

@ -0,0 +1,5 @@
hscommon.build
==============
.. automodule:: hscommon.build
:members:

View File

@ -0,0 +1,5 @@
hscommon.conflict
=================
.. automodule:: hscommon.conflict
:members:

View File

@ -0,0 +1,5 @@
hscommon.desktop
================
.. automodule:: hscommon.desktop
:members:

View File

@ -0,0 +1,12 @@
hscommon
========
.. toctree::
:maxdepth: 2
build
conflict
desktop
notify
path
util

View File

@ -0,0 +1,5 @@
hscommon.notify
===============
.. automodule:: hscommon.notify
:members:

View File

@ -0,0 +1,5 @@
hscommon.path
=============
.. automodule:: hscommon.path
:members:

View File

@ -0,0 +1,5 @@
hscommon.util
=============
.. automodule:: hscommon.util
:members:

View File

@ -53,9 +53,5 @@ API
.. toctree::
:maxdepth: 2
core/app
core/fs
core/engine
core/directories
core/results
core/gui
core/index
hscommon/index

View File

@ -1,9 +1,3 @@
The documentation has to be built with Sphinx. You can get Sphinx at http://sphinx.pocoo.org/
Once you installed it, you can build the documentation with:
cd docs
sphinx-build . ../docs_html
The reason why you have to move in 'docs' is because hscommon.io conflicts with the builtin 'io'
module. The documentation is also available online at http://www.hardcoded.net/docs/hscommon
This module is common code used in all Hardcoded Software applications. It has no stable API so
it is not recommended to actually depend on it. But if you want to copy bits and pieces for your own
apps, be my guest.

View File

@ -6,6 +6,9 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
"""This module is a collection of function to help in HS apps build process.
"""
import os
import sys
import os.path as op
@ -26,6 +29,8 @@ from .plat import ISWINDOWS
from .util import modified_after, find_in_path, ensure_folder, delete_files_with_pattern
def print_and_do(cmd):
"""Prints ``cmd`` and executes it in the shell.
"""
print(cmd)
p = Popen(cmd, shell=True)
return p.wait()
@ -125,6 +130,10 @@ def package_cocoa_app_in_dmg(app_path, destfolder, args):
build_dmg(app_path, destfolder)
def build_dmg(app_path, destfolder):
"""Builds a DMG volume with application at ``app_path`` and puts it in ``dest_path``.
The name of the resulting DMG volume is determined by the app's name and version.
"""
print(repr(op.join(app_path, 'Contents', 'Info.plist')))
plist = plistlib.readPlist(op.join(app_path, 'Contents', 'Info.plist'))
workpath = tempfile.mkdtemp()
@ -153,7 +162,7 @@ sysconfig.get_config_h_filename = lambda: op.join(op.dirname(__file__), 'pyconfi
""")
def add_to_pythonpath(path):
"""Adds `path` to both PYTHONPATH env and sys.path.
"""Adds ``path`` to both ``PYTHONPATH`` env and ``sys.path``.
"""
abspath = op.abspath(path)
pythonpath = os.environ.get('PYTHONPATH', '')
@ -166,6 +175,12 @@ def add_to_pythonpath(path):
# in setuptools. We copy the packages *without data* in a build folder and then build the plugin
# from there.
def copy_packages(packages_names, dest, create_links=False, extra_ignores=None):
"""Copy python packages ``packages_names`` to ``dest``, spurious data.
Copy will happen without tests, testdata, mercurial data or C extension module source with it.
``py2app`` include and exclude rules are **quite** funky, and doing this is the only reliable
way to make sure we don't end up with useless stuff in our app.
"""
if ISWINDOWS:
create_links = False
if not extra_ignores:

View File

@ -6,6 +6,10 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
"""When you have to deal with names that have to be unique and can conflict together, you can use
this module that deals with conflicts by prepending unique numbers in ``[]`` brackets to the name.
"""
import re
import os
import shutil
@ -18,7 +22,7 @@ from .path import Path, pathify
re_conflict = re.compile(r'^\[\d{3}\d*\] ')
def get_conflicted_name(other_names, name):
"""Returns name with a [000] number in front of it.
"""Returns name with a ``[000]`` number in front of it.
The number between brackets depends on how many conlicted filenames
there already are in other_names.
@ -34,16 +38,21 @@ def get_conflicted_name(other_names, name):
i += 1
def get_unconflicted_name(name):
"""Returns ``name`` without ``[]`` brackets.
Brackets which, of course, might have been added by func:`get_conflicted_name`.
"""
return re_conflict.sub('',name,1)
def is_conflicted(name):
"""Returns whether ``name`` is prepended with a bracketed number.
"""
return re_conflict.match(name) is not None
@pathify
def _smart_move_or_copy(operation, source_path: Path, dest_path: Path):
''' Use move() or copy() to move and copy file with the conflict management, but without the
slowness of the fs system.
'''
"""Use move() or copy() to move and copy file with the conflict management.
"""
if dest_path.isdir() and not source_path.isdir():
dest_path = dest_path[source_path.name]
if dest_path.exists():
@ -54,9 +63,13 @@ def _smart_move_or_copy(operation, source_path: Path, dest_path: Path):
operation(str(source_path), str(dest_path))
def smart_move(source_path, dest_path):
"""Same as :func:`smart_copy`, but it moves files instead.
"""
_smart_move_or_copy(shutil.move, source_path, dest_path)
def smart_copy(source_path, dest_path):
"""Copies ``source_path`` to ``dest_path``, recursively and with conflict resolution.
"""
try:
_smart_move_or_copy(shutil.copy, source_path, dest_path)
except IOError as e:

View File

@ -6,6 +6,10 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
"""This module facilitates currencies management. It exposes :class:`Currency` which lets you
easily figure out their exchange value.
"""
import os
from datetime import datetime, date, timedelta
import logging
@ -17,6 +21,21 @@ from .path import Path
from .util import iterdaterange
class Currency:
"""Represents a currency and allow easy exchange rate lookups.
A ``Currency`` instance is created with either a 3-letter ISO code or with a full name. If it's
present in the database, an instance will be returned. If not, ``ValueError`` is raised. The
easiest way to access a currency instance, however, if by using module-level constants. For
example::
>>> from hscommon.currency import USD, EUR
>>> from datetime import date
>>> USD.value_in(EUR, date.today())
0.6339119851386843
Unless a :class:`RatesDB` global instance is set through :meth:`Currency.set_rate_db` however,
only fallback values will be used as exchange rates.
"""
all = []
by_code = {}
by_name = {}
@ -68,12 +87,16 @@ class Currency:
@staticmethod
def set_rates_db(db):
"""Sets a new currency ``RatesDB`` instance to be used with all ``Currency`` instances.
"""
Currency.rates_db = db
@staticmethod
def get_rates_db():
"""Returns the current ``RatesDB`` instance.
"""
if Currency.rates_db is None:
Currency.rates_db = RatesDB() # Make sure we always have some db to work with
Currency.rates_db = RatesDB() # Make sure we always have some db to work with
return Currency.rates_db
def rates_date_range(self):
@ -372,7 +395,12 @@ class RatesDB:
self._cache = {}
def date_range(self, currency_code):
"""Returns (start, end) of the cached rates for currency"""
"""Returns (start, end) of the cached rates for currency.
Returns a tuple ``(start_date, end_date)`` representing dates covered in the database for
currency ``currency_code``. If there are gaps, they are not accounted for (subclasses that
automatically update themselves are not supposed to introduce gaps in the db).
"""
sql = "select min(date), max(date) from rates where currency = '%s'" % currency_code
cur = self._execute(sql)
start, end = cur.fetchone()

View File

@ -1,25 +0,0 @@
==========================================
:mod:`build` - Build utilities for HS apps
==========================================
This module is a collection of function to help in HS apps build process.
.. function:: print_and_do(cmd)
Prints ``cmd`` and executes it in the shell.
.. function:: build_all_qt_ui(base_dir='.')
Calls Qt's ``pyuic4`` for each file in ``base_dir`` with a ".ui" extension. The resulting file is saved under ``{base_name}_ui.py``.
.. function:: build_dmg(app_path, dest_path)
Builds a DMG volume with application at ``app_path`` and puts it in ``dest_path``. The name of the resulting DMG volume is determined by the app's name and version.
.. function:: add_to_pythonpath(path)
Adds ``path`` to both ``PYTHONPATH`` env variable and ``sys.path``.
.. function:: copy_packages(packages_names, dest)
Copy python packages ``packages_names`` to ``dest``, but without tests, testdata, mercurial data or C extension module source with it. ``py2app`` include and exclude rules are **quite** funky, and doing this is the only reliable way to make sure we don;t end up with useless stuff in our app.

View File

@ -1,194 +0,0 @@
# -*- coding: utf-8 -*-
#
# hscommon documentation build configuration file, created by
# sphinx-quickstart on Fri Mar 12 16:00:37 2010.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.append(os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = []
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'hscommon'
copyright = '2011, Hardcoded Software'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '1.0.0'
# The full version, including alpha/beta/rc tags.
release = '1.0.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of documents that shouldn't be included in the build.
#unused_docs = []
# List of directories, relative to source directory, that shouldn't be searched
# for source files.
exclude_trees = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
# html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_use_modindex = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = ''
# Output file base name for HTML help builder.
htmlhelp_basename = 'hscommondoc'
# -- Options for LaTeX output --------------------------------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
#latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'hscommon.tex', 'hscommon Documentation',
'Hardcoded Software', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# Additional stuff for the LaTeX preamble.
#latex_preamble = ''
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_use_modindex = True

View File

@ -1,27 +0,0 @@
===================================================
:mod:`conflict` - Detect and resolve name conflicts
===================================================
.. module:: conflict
When you have to deal with names that have to be unique and can conflict together, you can use this module that deals with conflicts by prepending unique numbers in ``[]`` brackets to the name.
.. function:: get_conflicted_name(other_names, name)
Returns a name based on ``name`` that is guaranteed not to be in ``other_names``. Name conflicts are resolved by prepending numbers in ``[]`` brackets to the name.
.. function:: get_unconflicted_name(name)
Returns ``name`` without ``[]`` brackets.
.. function:: is_conflicted(name)
Returns whether ``name`` is prepended with a bracketed number.
.. function:: smart_copy(source_path, dest_path)
Copies ``source_path`` to ``dest_path``, recursively. However, it does conflict resolution using functions in this module.
.. function:: smart_move(source_path, dest_path)
Same as :func:`smart_copy`, but it moves files instead.

View File

@ -1,62 +0,0 @@
===================================
:mod:`currency` - Manage currencies
===================================
This module facilitates currencies management. It exposes :class:`Currency` which lets you easily figure out their exchange value.
The ``Currency`` class
======================
.. class:: Currency(code=None, name=None)
A ``Currency`` instance is created with either a 3-letter ISO code or with a full name. If it's present in the database, an instance will be returned. If not, ``ValueError`` is raised. The easiest way to access a currency instance, however, if by using module-level constants. For example::
>>> from hscommon.currency import USD, EUR
>>> from datetime import date
>>> USD.value_in(EUR, date.today())
0.6339119851386843
Unless a :class:`currency.RatesDB` global instance is set through :meth:`Currency.set_rate_db` however, only fallback values will be used as exchange rates.
.. staticmethod:: Currency.register(code, name, exponent=2, fallback_rate=1)
Register a new currency in the currency list.
.. staticmethod:: Currency.set_rates_db(db)
Sets a new currency ``RatesDB`` instance to be used with all ``Currency`` instances.
.. staticmethod:: Currency.set_rates_db()
Returns the current ``RatesDB`` instance.
.. method:: Currency.rates_date_range()
Returns the range of date for which rates are available for this currency.
.. method:: Currency.value_in(currency, date)
Returns the value of this currency in terms of the other currency on the given date.
.. method:: Currency.set_CAD_value(value, date)
Sets currency's value in CAD on the given date.
The ``RatesDB`` class
=====================
.. class:: RatesDB(db_or_path=':memory:')
A sqlite database that stores currency/date/value pairs, "value" being the value of the currency in CAD at the given date. Currencies are referenced by their 3-letter ISO code in the database and it its arguments (so ``currency_code`` arguments must be 3-letter strings).
.. method:: RatesDB.date_range(currency_code)
Returns a tuple ``(start_date, end_date)`` representing dates covered in the database for currency ``currency_code``. If there are gaps, they are not accounted for (subclasses that automatically update themselves are not supposed to introduce gaps in the db).
.. method:: RatesDB.get_rate(date, currency1_code, currency2_code)
Returns the exchange rate between currency1 and currency2 for date. The rate returned means '1 unit of currency1 is worth X units of currency2'. The rate of the nearest date that is smaller than 'date' is returned. If there is none, a seek for a rate with a higher date will be made.
.. method:: RatesDB.set_CAD_value(date, currency_code, value)
Sets the CAD value of ``currency_code`` at ``date`` to ``value`` in the database.

View File

@ -1,32 +0,0 @@
==============================================
hscommon - Common code used throughout HS apps
==============================================
:Author: `Hardcoded Software <http://www.hardcoded.net>`_
:Dev website: http://hg.hardcoded.net/hscommon
:License: BSD License
Introduction
============
``hscommon`` is a collection of tools used throughout HS apps.
Dependencies
============
Python 3.1 is required. `py.test <http://pytest.org/>`_ is required to run the tests.
API Documentation
=================
.. toctree::
:maxdepth: 2
build
conflict
currency
notify
path
reg
sqlite
util

View File

@ -1,26 +0,0 @@
==========================================
:mod:`notify` - Simple notification system
==========================================
.. module:: notify
This module is a brain-dead simple notification system involving a :class:`Broadcaster` and a :class:`Listener`. A listener can only listen to one broadcaster. A broadcaster can have multiple listeners. If the listener is connected, whenever the broadcaster calls :meth:`~Broadcaster.notify`, the method with the same name as the broadcasted message is called on the listener.
.. class:: Broadcaster
.. method:: notify(msg)
Notify all connected listeners of ``msg``. That means that each listeners will have their method with the same name as ``msg`` called.
.. class:: Listener(broadcaster)
A listener is initialized with the broadcaster it's going to listen to. Initially, it is not connected.
.. method:: connect()
Connects the listener to its broadcaster.
.. method:: disconnect()
Disconnects the listener from its broadcaster.

View File

@ -1,13 +0,0 @@
========================================
:mod:`path` - Work with paths
========================================
.. module:: path
.. class:: Path(value, separator=None)
``Path`` instances can be created from strings, other ``Path`` instances or tuples. If ``separator`` is not specified, the one from the OS is used. Once created, paths can be manipulated like a tuple, each element being an element of the path. It makes a few common operations easy, such as getting the filename (``path[-1]``) or the directory name or parent (``path[:-1]``).
HS apps pretty much always refer to ``Path`` instances when a variable name ends with ``path``. If a variable is of another type, that type is usually explicited in the name.
To make common operations (from ``os.path``, ``os`` and ``shutil``) convenient, the :mod:`io` module wraps these functions and converts paths to strings.

View File

@ -1,25 +0,0 @@
========================================
:mod:`reg` - Manage app registration
========================================
.. module:: reg
.. class:: RegistrableApplication
HS main application classes subclass this. It provides an easy interface for managing whether the app should be in demo mode or not.
.. method:: _setup_as_registered()
Virtual. This is called whenever the app is unlocked. This is the one place to put code that changes to UI to indicate that the app is unlocked.
.. method:: validate_code(code, email)
Validates ``code`` with email. If it's valid, it does nothing. Otherwise, it raises ``InvalidCodeError`` with a message indicating why it's invalid (wrong product, wrong code format, fields swapped).
.. method:: set_registration(code, email)
If ``code`` and ``email`` are valid, sets ``registered`` to True as well as ``registration_code`` and ``registration_email`` and then calls :meth:`_setup_as_registered`.
.. exception:: InvalidCodeError
Raised during :meth:`RegistrableApplication.validate_code`.

View File

@ -1,9 +0,0 @@
==========================================
:mod:`sqlite` - Threaded sqlite connection
==========================================
.. module:: sqlite
.. class:: ThreadedConn(dbname, autocommit)
``sqlite`` connections can't be used across threads. ``TheadedConn`` opens a sqlite connection in its own thread and sends it queries through a queue, making it suitable in multi-threaded environment.

View File

@ -1,88 +0,0 @@
========================================
:mod:`util` - Miscellaneous utilities
========================================
.. module:: misc
.. function:: nonone(value, replace_value)
Returns ``value`` if value is not None. Returns ``replace_value`` otherwise.
.. function:: dedupe(iterable)
Returns a list of elements in ``iterable`` with all dupes removed. The order of the elements is preserved.
.. function:: flatten(iterables, start_with=None)
Takes the list of iterable ``iterables`` and returns a list containing elements of every iterable.
If ``start_with`` is not None, the result will start with ``start_with`` items, exactly as if ``start_with`` would be the first item of lists.
.. function:: first(iterable)
Returns the first item of ``iterable`` or ``None`` if empty.
.. function:: tryint(value, default=0)
Tries to convert ``value`` to in ``int`` and returns ``default`` if it fails.
.. function:: escape(s, to_escape, escape_with='\\')
Returns ``s`` with characters in ``to_escape`` all prepended with ``escape_with``.
.. function:: format_size(size, decimal=0, forcepower=-1, showdesc=True)
Transform a byte count ``size`` in a formatted string (KB, MB etc..). ``decimal`` is the number digits after the dot. ``forcepower`` is the desired suffix. 0 is B, 1 is KB, 2 is MB etc.. if kept at -1, the suffix will be automatically chosen (so the resulting number is always below 1024). If ``showdesc`` is True, the suffix will be shown after the number. Usage example::
>>> format_size(1234, decimal=2, showdesc=True)
'1.21 KB'
.. function:: format_time(seconds, with_hours=True)
Transforms seconds in a hh:mm:ss string.
If `with_hours` if false, the format is mm:ss.
.. function:: format_time_decimal(seconds)
Transforms seconds in a strings like '3.4 minutes'.
.. function:: get_file_ext(filename)
Returns the lowercase extension part of ``filename``, without the dot.
.. function:: pluralize(number, word, decimals=0, plural_word=None)
Returns a string with ``number`` in front of ``word``, and adds a 's' to ``word`` if ``number`` > 1. If ``plural_word`` is defined, it will replace ``word`` in plural cases instead of appending a 's'.
.. function:: rem_file_ext(filename)
Returns ``filename`` without extension.
.. function:: multi_replace(s, replace_from, replace_to='')
A function like str.replace() with multiple replacements. ``replace_from`` is a list of things you want to replace (Ex: ``['a','bc','d']``). ``replace_to`` is a list of what you want to replace to. If ``replace_to`` is a list and has the same length as ``replace_from``, ``replace_from`` items will be translated to corresponding ``replace_to``. A ``replace_to`` list must have the same length as ``replace_from``. If ``replace_to`` is a string, all ``replace_from`` occurences will be replaced by that string. ``replace_from`` can also be a string. If it is, every char in it will be translated as if ``replace_from`` would be a list of chars. If ``replace_to`` is a string and has the same length as ``replace_from``, it will be transformed into a list.
.. function:: open_if_filename(infile, mode='rb')
If ``infile`` is a string, it opens and returns it. If it's already a file object, it simply returns it. This function returns ``(file, should_close_flag)``. The should_close_flag is True is a file has effectively been opened (if we already pass a file object, we assume that the responsibility for closing the file has already been taken). Example usage::
fp, shouldclose = open_if_filename(infile)
dostuff()
if shouldclose:
fp.close()
.. class:: FileOrPath(file_or_path, mode='rb')
Does the same as :func:`open_if_filename`, but it can be used with a ``with`` statement. Example::
with FileOrPath(infile):
dostuff()
.. function:: delete_if_empty(path, files_to_delete=[])
Same as with :func:`clean_empty_dirs`, but not recursive.
.. function:: modified_after(first_path, second_path)
Returns True if ``first_path``'s mtime is higher than ``second_path``'s mtime.

View File

@ -4,9 +4,19 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
"""Very simple inter-object notification system.
This module is a brain-dead simple notification system involving a :class:`Broadcaster` and a
:class:`Listener`. A listener can only listen to one broadcaster. A broadcaster can have multiple
listeners. If the listener is connected, whenever the broadcaster calls :meth:`~Broadcaster.notify`,
the method with the same name as the broadcasted message is called on the listener.
"""
from collections import defaultdict
class Broadcaster:
"""Broadcasts messages that are received by all listeners.
"""
def __init__(self):
self.listeners = set()
@ -14,6 +24,10 @@ class Broadcaster:
self.listeners.add(listener)
def notify(self, msg):
"""Notify all connected listeners of ``msg``.
That means that each listeners will have their method with the same name as ``msg`` called.
"""
for listener in self.listeners.copy(): # listeners can change during iteration
if listener in self.listeners: # disconnected during notification
listener.dispatch(msg)
@ -23,6 +37,8 @@ class Broadcaster:
class Listener:
"""A listener is initialized with the broadcaster it's going to listen to. Initially, it is not connected.
"""
def __init__(self, broadcaster):
self.broadcaster = broadcaster
self._bound_notifications = defaultdict(list)
@ -38,9 +54,13 @@ class Listener:
self._bound_notifications[message].append(func)
def connect(self):
"""Connects the listener to its broadcaster.
"""
self.broadcaster.add_listener(self)
def disconnect(self):
"""Disconnects the listener from its broadcaster.
"""
self.broadcaster.remove_listener(self)
def dispatch(self, msg):

View File

@ -18,20 +18,12 @@ from inspect import signature
class Path(tuple):
"""A handy class to work with paths.
path[index] returns a string
path[start:stop] returns a Path
start and stop can be int, but the can also be path instances. When start
or stop are Path like in refpath[p1:p2], it is the same thing as typing
refpath[len(p1):-len(p2)], except that it will only slice out stuff that are
equal. For example, 'a/b/c/d'['a/z':'z/d'] returns 'b/c', not ''.
See the test units for more details.
We subclass ``tuple``, each element of the tuple represents an element of the path.
You can use the + operator, which is the same thing as with tuples, but
returns a Path.
In HS applications, all paths variable should be Path instances. These Path instances should
be converted to str only at the last moment (when it is needed in an external function, such
as os.rename)
* ``Path('/foo/bar/baz')[1]`` --> ``'bar'``
* ``Path('/foo/bar/baz')[1:2]`` --> ``Path('bar/baz')``
* ``Path('/foo/bar')['baz']`` --> ``Path('/foo/bar/baz')``
* ``str(Path('/foo/bar/baz'))`` --> ``'/foo/bar/baz'``
"""
# Saves a little bit of memory usage
__slots__ = ()
@ -135,10 +127,18 @@ class Path(tuple):
return str(self).encode(sys.getfilesystemencoding())
def parent(self):
"""Returns the parent path.
``Path('/foo/bar/baz').parent()`` --> ``Path('/foo/bar')``
"""
return self[:-1]
@property
def name(self):
"""Last element of the path (filename), with extension.
``Path('/foo/bar/baz').name`` --> ``'baz'``
"""
return self[-1]
# OS method wrappers

View File

@ -114,6 +114,10 @@ class _ActualThread(threading.Thread):
class ThreadedConn:
"""``sqlite`` connections can't be used across threads. ``TheadedConn`` opens a sqlite
connection in its own thread and sends it queries through a queue, making it suitable in
multi-threaded environment.
"""
def __init__(self, dbname, autocommit):
self._t = _ActualThread(dbname, autocommit)
self.lastrowid = -1

View File

@ -18,14 +18,16 @@ from datetime import timedelta
from .path import Path, pathify, log_io_error
def nonone(value, replace_value):
''' Returns value if value is not None. Returns replace_value otherwise.
'''
"""Returns ``value`` if ``value`` is not ``None``. Returns ``replace_value`` otherwise.
"""
if value is None:
return replace_value
else:
return value
def tryint(value, default=0):
"""Tries to convert ``value`` to in ``int`` and returns ``default`` if it fails.
"""
try:
return int(value)
except (TypeError, ValueError):
@ -39,8 +41,10 @@ def minmax(value, min_value, max_value):
#--- Sequence related
def dedupe(iterable):
'''Returns a list of elements in iterable with all dupes removed.
'''
"""Returns a list of elements in ``iterable`` with all dupes removed.
The order of the elements is preserved.
"""
result = []
seen = {}
for item in iterable:
@ -51,11 +55,11 @@ def dedupe(iterable):
return result
def flatten(iterables, start_with=None):
'''Takes a list of lists 'lists' and returns a list containing elements of every list.
"""Takes a list of lists ``iterables`` and returns a list containing elements of every list.
If start_with is not None, the result will start with start_with items, exactly as if
start_with would be the first item of lists.
'''
If ``start_with`` is not ``None``, the result will start with ``start_with`` items, exactly as
if ``start_with`` would be the first item of lists.
"""
result = []
if start_with:
result.extend(start_with)
@ -64,7 +68,7 @@ def flatten(iterables, start_with=None):
return result
def first(iterable):
"""Returns the first item of 'iterable'
"""Returns the first item of ``iterable``.
"""
try:
return next(iter(iterable))
@ -116,10 +120,13 @@ def trailiter(iterable, skipfirst=False):
#--- String related
def escape(s, to_escape, escape_with='\\'):
"""Returns ``s`` with characters in ``to_escape`` all prepended with ``escape_with``.
"""
return ''.join((escape_with + c if c in to_escape else c) for c in s)
def get_file_ext(filename):
"""Returns the lowercase extension part of filename, without the dot."""
"""Returns the lowercase extension part of filename, without the dot.
"""
pos = filename.rfind('.')
if pos > -1:
return filename[pos + 1:].lower()
@ -127,7 +134,8 @@ def get_file_ext(filename):
return ''
def rem_file_ext(filename):
"""Returns the filename without extension."""
"""Returns the filename without extension.
"""
pos = filename.rfind('.')
if pos > -1:
return filename[:pos]
@ -135,12 +143,13 @@ def rem_file_ext(filename):
return filename
def pluralize(number, word, decimals=0, plural_word=None):
"""Returns a string with number in front of s, and adds a 's' to s if number > 1
"""Returns a pluralized string with ``number`` in front of ``word``.
number: The number to go in front of s
word: The word to go after number
decimals: The number of digits after the dot
plural_word: If the plural rule for word is more complex than adding a 's', specify a plural
Adds a 's' to s if ``number`` > 1.
``number``: The number to go in front of s
``word``: The word to go after number
``decimals``: The number of digits after the dot
``plural_word``: If the plural rule for word is more complex than adding a 's', specify a plural
"""
number = round(number, decimals)
format = "%%1.%df %%s" % decimals
@ -154,7 +163,7 @@ def pluralize(number, word, decimals=0, plural_word=None):
def format_time(seconds, with_hours=True):
"""Transforms seconds in a hh:mm:ss string.
If `with_hours` if false, the format is mm:ss.
If ``with_hours`` if false, the format is mm:ss.
"""
minus = seconds < 0
if minus:
@ -171,7 +180,8 @@ def format_time(seconds, with_hours=True):
return r
def format_time_decimal(seconds):
"""Transforms seconds in a strings like '3.4 minutes'"""
"""Transforms seconds in a strings like '3.4 minutes'.
"""
minus = seconds < 0
if minus:
seconds *= -1
@ -193,11 +203,15 @@ SIZE_VALS = tuple(1024 ** i for i in range(1,9))
def format_size(size, decimal=0, forcepower=-1, showdesc=True):
"""Transform a byte count in a formatted string (KB, MB etc..).
size is the number of bytes to format.
decimal is the number digits after the dot.
forcepower is the desired suffix. 0 is B, 1 is KB, 2 is MB etc.. if kept at -1, the suffix will
be automatically chosen (so the resulting number is always below 1024).
if showdesc is True, the suffix will be shown after the number.
``size`` is the number of bytes to format.
``decimal`` is the number digits after the dot.
``forcepower`` is the desired suffix. 0 is B, 1 is KB, 2 is MB etc.. if kept at -1, the suffix
will be automatically chosen (so the resulting number is always below 1024).
if ``showdesc`` is ``True``, the suffix will be shown after the number.
Usage example::
>>> format_size(1234, decimal=2, showdesc=True)
'1.21 KB'
"""
if forcepower < 0:
i = 0
@ -234,16 +248,16 @@ def remove_invalid_xml(s, replace_with=' '):
def multi_replace(s, replace_from, replace_to=''):
"""A function like str.replace() with multiple replacements.
replace_from is a list of things you want to replace. Ex: ['a','bc','d']
replace_to is a list of what you want to replace to.
If replace_to is a list and has the same length as replace_from, replace_from
items will be translated to corresponding replace_to. A replace_to list must
have the same length as replace_from
If replace_to is a basestring, all replace_from occurence will be replaced
``replace_from`` is a list of things you want to replace. Ex: ['a','bc','d']
``replace_to`` is a list of what you want to replace to.
If ``replace_to`` is a list and has the same length as ``replace_from``, ``replace_from``
items will be translated to corresponding ``replace_to``. A ``replace_to`` list must
have the same length as ``replace_from``
If ``replace_to`` is a string, all ``replace_from`` occurence will be replaced
by that string.
replace_from can also be a str. If it is, every char in it will be translated
as if replace_from would be a list of chars. If replace_to is a str and has
the same length as replace_from, it will be transformed into a list.
``replace_from`` can also be a str. If it is, every char in it will be translated
as if ``replace_from`` would be a list of chars. If ``replace_to`` is a str and has
the same length as ``replace_from``, it will be transformed into a list.
"""
if isinstance(replace_to, str) and (len(replace_from) != len(replace_to)):
replace_to = [replace_to for r in replace_from]
@ -298,8 +312,8 @@ def find_in_path(name, paths=None):
@log_io_error
@pathify
def delete_if_empty(path: Path, files_to_delete=[]):
''' Deletes the directory at 'path' if it is empty or if it only contains files_to_delete.
'''
"""Deletes the directory at 'path' if it is empty or if it only contains files_to_delete.
"""
if not path.exists() or not path.isdir():
return
contents = path.listdir()
@ -311,10 +325,16 @@ def delete_if_empty(path: Path, files_to_delete=[]):
return True
def open_if_filename(infile, mode='rb'):
"""
infile can be either a string or a file-like object.
if it is a string, a file will be opened with mode.
Returns a tuple (shouldbeclosed,infile) infile is a file object
"""If ``infile`` is a string, it opens and returns it. If it's already a file object, it simply returns it.
This function returns ``(file, should_close_flag)``. The should_close_flag is True is a file has
effectively been opened (if we already pass a file object, we assume that the responsibility for
closing the file has already been taken). Example usage::
fp, shouldclose = open_if_filename(infile)
dostuff()
if shouldclose:
fp.close()
"""
if isinstance(infile, Path):
return (infile.open(mode), True)
@ -349,6 +369,13 @@ def delete_files_with_pattern(folder_path, pattern, recursive=True):
delete_files_with_pattern(p, pattern, True)
class FileOrPath:
"""Does the same as :func:`open_if_filename`, but it can be used with a ``with`` statement.
Example::
with FileOrPath(infile):
dostuff()
"""
def __init__(self, file_or_path, mode='rb'):
self.file_or_path = file_or_path
self.mode = mode