Added hscommon repo as a subtree

This commit is contained in:
Virgil Dupras 2013-06-22 21:32:23 -04:00
parent 95623f9b47
commit 94a469205a
62 changed files with 6553 additions and 0 deletions

5
hscommon/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
*.pyc
*.mo
*.so
.DS_Store
/docs_html

8
hscommon/.tx/config Normal file
View File

@ -0,0 +1,8 @@
[main]
host = https://www.transifex.net
[hscommon.hscommon]
file_filter = locale/<lang>/LC_MESSAGES/hscommon.po
source_file = locale/hscommon.pot
source_lang = en
type = PO

10
hscommon/LICENSE Normal file
View File

@ -0,0 +1,10 @@
Copyright 2013, Hardcoded Software Inc., http://www.hardcoded.net
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

9
hscommon/README Normal file
View File

@ -0,0 +1,9 @@
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

0
hscommon/__init__.py Executable file
View File

466
hscommon/build.py Normal file
View File

@ -0,0 +1,466 @@
# Created By: Virgil Dupras
# Created On: 2009-03-03
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
import os
import sys
import os.path as op
import shutil
import tempfile
import plistlib
from subprocess import Popen
import re
import importlib
from datetime import datetime
import glob
import sysconfig
import modulefinder
from setuptools import setup, Extension
from .plat import ISWINDOWS
from .util import modified_after, find_in_path, ensure_folder, delete_files_with_pattern
def print_and_do(cmd):
print(cmd)
p = Popen(cmd, shell=True)
return p.wait()
def _perform(src, dst, action, actionname):
if not op.lexists(src):
print("Copying %s failed: it doesn't exist." % src)
return
if op.lexists(dst):
if op.isdir(dst):
shutil.rmtree(dst)
else:
os.remove(dst)
print('%s %s --> %s' % (actionname, src, dst))
action(src, dst)
def copy_file_or_folder(src, dst):
if op.isdir(src):
shutil.copytree(src, dst, symlinks=True)
else:
shutil.copy(src, dst)
def move(src, dst):
_perform(src, dst, os.rename, 'Moving')
def copy(src, dst):
_perform(src, dst, copy_file_or_folder, 'Copying')
def symlink(src, dst):
_perform(src, dst, os.symlink, 'Symlinking')
def hardlink(src, dst):
_perform(src, dst, os.link, 'Hardlinking')
def _perform_on_all(pattern, dst, action):
# pattern is a glob pattern, example "folder/foo*". The file is moved directly in dst, no folder
# structure from src is kept.
filenames = glob.glob(pattern)
for fn in filenames:
destpath = op.join(dst, op.basename(fn))
action(fn, destpath)
def move_all(pattern, dst):
_perform_on_all(pattern, dst, move)
def copy_all(pattern, dst):
_perform_on_all(pattern, dst, copy)
def ensure_empty_folder(path):
"""Make sure that the path exists and that it's an empty folder.
"""
if op.exists(path):
shutil.rmtree(path)
os.mkdir(path)
def filereplace(filename, outfilename=None, **kwargs):
"""Reads `filename`, replaces all {variables} in kwargs, and writes the result to `outfilename`.
"""
if outfilename is None:
outfilename = filename
fp = open(filename, 'rt', encoding='utf-8')
contents = fp.read()
fp.close()
# We can't use str.format() because in some files, there might be {} characters that mess with it.
for key, item in kwargs.items():
contents = contents.replace('{{{}}}'.format(key), item)
fp = open(outfilename, 'wt', encoding='utf-8')
fp.write(contents)
fp.close()
def get_module_version(modulename):
mod = importlib.import_module(modulename)
return mod.__version__
def setup_package_argparser(parser):
parser.add_argument('--sign', dest='sign_identity',
help="Sign app under specified identity before packaging (OS X only)")
parser.add_argument('--nosign', action='store_true', dest='nosign',
help="Don't sign the packaged app (OS X only)")
parser.add_argument('--src-pkg', action='store_true', dest='src_pkg',
help="Build a tar.gz of the current source.")
# `args` come from an ArgumentParser updated with setup_package_argparser()
def package_cocoa_app_in_dmg(app_path, destfolder, args):
# Rather than signing our app in XCode during the build phase, we sign it during the package
# phase because running the app before packaging can modify it and we want to be sure to have
# a valid signature.
if args.sign_identity:
sign_identity = "Developer ID Application: {}".format(args.sign_identity)
result = print_and_do('codesign --force --sign "{}" "{}"'.format(sign_identity, app_path))
if result != 0:
print("ERROR: Signing failed. Aborting packaging.")
return
elif not args.nosign:
print("ERROR: Either --nosign or --sign argument required.")
return
build_dmg(app_path, destfolder)
def build_dmg(app_path, destfolder):
print(repr(op.join(app_path, 'Contents', 'Info.plist')))
plist = plistlib.readPlist(op.join(app_path, 'Contents', 'Info.plist'))
workpath = tempfile.mkdtemp()
dmgpath = op.join(workpath, plist['CFBundleName'])
os.mkdir(dmgpath)
print_and_do('cp -R "%s" "%s"' % (app_path, dmgpath))
print_and_do('ln -s /Applications "%s"' % op.join(dmgpath, 'Applications'))
dmgname = '%s_osx_%s.dmg' % (plist['CFBundleName'].lower().replace(' ', '_'), plist['CFBundleVersion'].replace('.', '_'))
print('Building %s' % dmgname)
# UDBZ = bzip compression. UDZO (zip compression) was used before, but it compresses much less.
print_and_do('hdiutil create "%s" -format UDBZ -nocrossdev -srcdir "%s"' % (op.join(destfolder, dmgname), dmgpath))
print('Build Complete')
def copy_sysconfig_files_for_embed(destpath):
# This normally shouldn't be needed for Python 3.3+.
makefile = sysconfig.get_makefile_filename()
configh = sysconfig.get_config_h_filename()
shutil.copy(makefile, destpath)
shutil.copy(configh, destpath)
with open(op.join(destpath, 'site.py'), 'w') as fp:
fp.write("""
import os.path as op
from distutils import sysconfig
sysconfig.get_makefile_filename = lambda: op.join(op.dirname(__file__), 'Makefile')
sysconfig.get_config_h_filename = lambda: op.join(op.dirname(__file__), 'pyconfig.h')
""")
def add_to_pythonpath(path):
"""Adds `path` to both PYTHONPATH env and sys.path.
"""
abspath = op.abspath(path)
pythonpath = os.environ.get('PYTHONPATH', '')
pathsep = ';' if ISWINDOWS else ':'
pythonpath = pathsep.join([abspath, pythonpath]) if pythonpath else abspath
os.environ['PYTHONPATH'] = pythonpath
sys.path.insert(1, abspath)
# This is a method to hack around those freakingly tricky data inclusion/exlusion rules
# 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):
if ISWINDOWS:
create_links = False
if not extra_ignores:
extra_ignores = []
ignore = shutil.ignore_patterns('.hg*', 'tests', 'testdata', 'modules', 'docs', 'locale', *extra_ignores)
for package_name in packages_names:
if op.exists(package_name):
source_path = package_name
else:
mod = __import__(package_name)
source_path = mod.__file__
if mod.__file__.endswith('__init__.py'):
source_path = op.dirname(source_path)
dest_name = op.basename(source_path)
dest_path = op.join(dest, dest_name)
if op.exists(dest_path):
if op.islink(dest_path):
os.unlink(dest_path)
else:
shutil.rmtree(dest_path)
print("Copying package at {0} to {1}".format(source_path, dest_path))
if create_links:
os.symlink(op.abspath(source_path), dest_path)
else:
if op.isdir(source_path):
shutil.copytree(source_path, dest_path, ignore=ignore)
else:
shutil.copy(source_path, dest_path)
def copy_qt_plugins(folder_names, dest): # This is only for Windows
from PyQt4.QtCore import QLibraryInfo
qt_plugin_dir = QLibraryInfo.location(QLibraryInfo.PluginsPath)
def ignore(path, names):
if path == qt_plugin_dir:
return [n for n in names if n not in folder_names]
else:
return [n for n in names if not n.endswith('.dll')]
shutil.copytree(qt_plugin_dir, dest, ignore=ignore)
def build_debian_changelog(changelogpath, destfile, pkgname, from_version=None,
distribution='precise', fix_version=None):
"""Builds a debian changelog out of a YAML changelog.
Use fix_version to patch the top changelog to that version (if, for example, there was a
packaging error and you need to quickly fix it)
"""
def desc2list(desc):
# We take each item, enumerated with the '*' character, and transform it into a list.
desc = desc.replace('\n', ' ')
desc = desc.replace(' ', ' ')
result = desc.split('*')
return [s.strip() for s in result if s.strip()]
ENTRY_MODEL = "{pkg} ({version}~{distribution}) {distribution}; urgency=low\n\n{changes}\n -- Virgil Dupras <hsoft@hardcoded.net> {date}\n\n"
CHANGE_MODEL = " * {description}\n"
changelogs = read_changelog_file(changelogpath)
if from_version:
# We only want logs from a particular version
for index, log in enumerate(changelogs):
if log['version'] == from_version:
changelogs = changelogs[:index+1]
break
if fix_version:
changelogs[0]['version'] = fix_version
rendered_logs = []
for log in changelogs:
version = log['version']
logdate = log['date']
desc = log['description']
rendered_date = logdate.strftime('%a, %d %b %Y 00:00:00 +0000')
rendered_descs = [CHANGE_MODEL.format(description=d) for d in desc2list(desc)]
changes = ''.join(rendered_descs)
rendered_log = ENTRY_MODEL.format(pkg=pkgname, version=version, changes=changes,
date=rendered_date, distribution=distribution)
rendered_logs.append(rendered_log)
result = ''.join(rendered_logs)
fp = open(destfile, 'w')
fp.write(result)
fp.close()
re_changelog_header = re.compile(r'=== ([\d.b]*) \(([\d\-]*)\)')
def read_changelog_file(filename):
def iter_by_three(it):
while True:
version = next(it)
date = next(it)
description = next(it)
yield version, date, description
with open(filename, 'rt', encoding='utf-8') as fp:
contents = fp.read()
splitted = re_changelog_header.split(contents)[1:] # the first item is empty
# splitted = [version1, date1, desc1, version2, date2, ...]
result = []
for version, date_str, description in iter_by_three(iter(splitted)):
date = datetime.strptime(date_str, '%Y-%m-%d').date()
d = {'date': date, 'date_str': date_str, 'version': version, 'description': description.strip()}
result.append(d)
return result
class OSXAppStructure:
def __init__(self, dest):
self.dest = dest
self.contents = op.join(dest, 'Contents')
self.macos = op.join(self.contents, 'MacOS')
self.resources = op.join(self.contents, 'Resources')
self.frameworks = op.join(self.contents, 'Frameworks')
self.infoplist = op.join(self.contents, 'Info.plist')
def create(self, infoplist):
ensure_empty_folder(self.dest)
os.makedirs(self.macos)
os.mkdir(self.resources)
os.mkdir(self.frameworks)
copy(infoplist, self.infoplist)
open(op.join(self.contents, 'PkgInfo'), 'wt').write("APPLxxxx")
def copy_executable(self, executable):
info = plistlib.readPlist(self.infoplist)
self.executablename = info['CFBundleExecutable']
self.executablepath = op.join(self.macos, self.executablename)
copy(executable, self.executablepath)
def copy_resources(self, *resources, use_symlinks=False):
for path in resources:
resource_dest = op.join(self.resources, op.basename(path))
action = symlink if use_symlinks else copy
action(op.abspath(path), resource_dest)
def copy_frameworks(self, *frameworks):
for path in frameworks:
framework_dest = op.join(self.frameworks, op.basename(path))
copy(path, framework_dest)
def create_osx_app_structure(dest, executable, infoplist, resources=None, frameworks=None,
symlink_resources=False):
# `dest`: A path to the destination .app folder
# `executable`: the path of the executable file that goes in "MacOS"
# `infoplist`: The path to your Info.plist file.
# `resources`: A list of paths of files or folders going in the "Resources" folder.
# `frameworks`: Same as above for "Frameworks".
# `symlink_resources`: If True, will symlink resources into the structure instead of copying them.
app = OSXAppStructure(dest, infoplist)
app.create()
app.copy_executable(executable)
app.copy_resources(*resources, use_symlinks=symlink_resources)
app.copy_frameworks(*frameworks)
class OSXFrameworkStructure:
def __init__(self, dest):
self.dest = dest
self.contents = op.join(dest, 'Versions', 'A')
self.resources = op.join(self.contents, 'Resources')
self.headers = op.join(self.contents, 'Headers')
self.infoplist = op.join(self.resources, 'Info.plist')
self._update_executable_path()
def _update_executable_path(self):
if not op.exists(self.infoplist):
self.executablename = self.executablepath = None
return
info = plistlib.readPlist(self.infoplist)
self.executablename = info['CFBundleExecutable']
self.executablepath = op.join(self.contents, self.executablename)
def create(self, infoplist):
ensure_empty_folder(self.dest)
os.makedirs(self.contents)
os.mkdir(self.resources)
os.mkdir(self.headers)
copy(infoplist, self.infoplist)
self._update_executable_path()
def create_symlinks(self):
# Only call this after create() and copy_executable()
rel = lambda path: op.relpath(path, self.dest)
os.symlink('A', op.join(self.dest, 'Versions', 'Current'))
os.symlink(rel(self.executablepath), op.join(self.dest, self.executablename))
os.symlink(rel(self.headers), op.join(self.dest, 'Headers'))
os.symlink(rel(self.resources), op.join(self.dest, 'Resources'))
def copy_executable(self, executable):
copy(executable, self.executablepath)
def copy_resources(self, *resources, use_symlinks=False):
for path in resources:
resource_dest = op.join(self.resources, op.basename(path))
action = symlink if use_symlinks else copy
action(op.abspath(path), resource_dest)
def copy_headers(self, *headers, use_symlinks=False):
for path in headers:
header_dest = op.join(self.headers, op.basename(path))
action = symlink if use_symlinks else copy
action(op.abspath(path), header_dest)
def build_cocoalib_xibless(dest='cocoa/autogen', withfairware=True):
import xibless
ensure_folder(dest)
FNPAIRS = [
('progress.py', 'ProgressController_UI'),
('error_report.py', 'HSErrorReportWindow_UI'),
]
if withfairware:
FNPAIRS += [
('fairware_about.py', 'HSFairwareAboutBox_UI'),
('demo_reminder.py', 'HSDemoReminder_UI'),
('enter_code.py', 'HSEnterCode_UI'),
]
else:
FNPAIRS += [
('about.py', 'HSAboutBox_UI'),
]
for srcname, dstname in FNPAIRS:
srcpath = op.join('cocoalib', 'ui', srcname)
dstpath = op.join(dest, dstname)
if modified_after(srcpath, dstpath + '.h'):
xibless.generate(srcpath, dstpath, localizationTable='cocoalib')
def copy_embeddable_python_dylib(dst):
runtime = op.join(sysconfig.get_config_var('PYTHONFRAMEWORKPREFIX'), sysconfig.get_config_var('LDLIBRARY'))
filedest = op.join(dst, 'Python')
shutil.copy(runtime, filedest)
os.chmod(filedest, 0o774) # We need write permission to use install_name_tool
cmd = 'install_name_tool -id @rpath/Python %s' % filedest
print_and_do(cmd)
def collect_stdlib_dependencies(script, dest_folder, extra_deps=None):
sysprefix = sys.prefix # could be a virtualenv
real_lib_prefix = sysconfig.get_config_var('LIBDEST')
def is_stdlib_path(path):
# A module path is only a stdlib path if it's in either sys.prefix or
# sysconfig.get_config_var('prefix') (the 2 are different if we are in a virtualenv) and if
# there's no "site-package in the path.
if not path:
return False
if 'site-package' in path:
return False
if not (path.startswith(sysprefix) or path.startswith(real_lib_prefix)):
return False
return True
ensure_folder(dest_folder)
mf = modulefinder.ModuleFinder()
mf.run_script(script)
modpaths = [mod.__file__ for mod in mf.modules.values()]
modpaths = filter(is_stdlib_path, modpaths)
for p in modpaths:
if p.startswith(real_lib_prefix):
relpath = op.relpath(p, real_lib_prefix)
elif p.startswith(sysprefix):
relpath = op.relpath(p, sysprefix)
assert relpath.startswith('lib/python3.') # we want to get rid of that lib/python3.x part
relpath = relpath[len('lib/python3.X/'):]
else:
raise AssertionError()
if relpath.startswith('lib-dynload'): # We copy .so files in lib-dynload directly in our dest
relpath = relpath[len('lib-dynload/'):]
if relpath.startswith('encodings') or relpath.startswith('distutils'):
# We force their inclusion later.
continue
dest_path = op.join(dest_folder, relpath)
ensure_folder(op.dirname(dest_path))
copy(p, dest_path)
# stringprep is used by encodings.
# We use real_lib_prefix with distutils because virtualenv messes with it and we need to refer
# to the original distutils folder.
FORCED_INCLUSION = ['encodings', 'stringprep', op.join(real_lib_prefix, 'distutils')]
if extra_deps:
FORCED_INCLUSION += extra_deps
copy_packages(FORCED_INCLUSION, dest_folder)
# There's a couple of rather big exe files in the distutils folder that we absolutely don't
# need. Remove them.
delete_files_with_pattern(op.join(dest_folder, 'distutils'), '*.exe')
def fix_qt_resource_file(path):
# pyrcc4 under Windows, if the locale is non-english, can produce a source file with a date
# containing accented characters. If it does, the encoding is wrong and it prevents the file
# from being correctly frozen by cx_freeze. To work around that, we open the file, strip all
# comments, and save.
with open(path, 'rb') as fp:
contents = fp.read()
lines = contents.split(b'\n')
lines = [l for l in lines if not l.startswith(b'#')]
with open(path, 'wb') as fp:
fp.write(b'\n'.join(lines))
def build_cocoa_ext(extname, dest, source_files, extra_frameworks=(), extra_includes=()):
extra_link_args = ["-framework", "CoreFoundation", "-framework", "Foundation"]
for extra in extra_frameworks:
extra_link_args += ['-framework', extra]
ext = Extension(extname, source_files, extra_link_args=extra_link_args, include_dirs=extra_includes)
setup(script_args=['build_ext', '--inplace'], ext_modules=[ext])
fn = extname + '.so'
assert op.exists(fn)
move(fn, op.join(dest, fn))

62
hscommon/conflict.py Normal file
View File

@ -0,0 +1,62 @@
# Created By: Virgil Dupras
# Created On: 2008-01-08
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
import re
from . import io
#This matches [123], but not [12] (3 digits being the minimum).
#It also matches [1234] [12345] etc..
#And only at the start of the string
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.
The number between brackets depends on how many conlicted filenames
there already are in other_names.
"""
name = get_unconflicted_name(name)
if name not in other_names:
return name
i = 0
while True:
newname = '[%03d] %s' % (i, name)
if newname not in other_names:
return newname
i += 1
def get_unconflicted_name(name):
return re_conflict.sub('',name,1)
def is_conflicted(name):
return re_conflict.match(name) is not None
def _smart_move_or_copy(operation, source_path, dest_path):
''' Use move() or copy() to move and copy file with the conflict management, but without the
slowness of the fs system.
'''
if io.isdir(dest_path) and not io.isdir(source_path):
dest_path = dest_path + source_path[-1]
if io.exists(dest_path):
filename = dest_path[-1]
dest_dir_path = dest_path[:-1]
newname = get_conflicted_name(io.listdir(dest_dir_path), filename)
dest_path = dest_dir_path + newname
operation(source_path, dest_path)
def smart_move(source_path, dest_path):
_smart_move_or_copy(io.move, source_path, dest_path)
def smart_copy(source_path, dest_path):
try:
_smart_move_or_copy(io.copy, source_path, dest_path)
except IOError as e:
if e.errno in {21, 13}: # it's a directory, code is 21 on OS X / Linux and 13 on Windows
_smart_move_or_copy(io.copytree, source_path, dest_path)
else:
raise

456
hscommon/currency.py Normal file
View File

@ -0,0 +1,456 @@
# Created By: Virgil Dupras
# Created On: 2008-04-20
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
from datetime import datetime, date, timedelta
import logging
import sqlite3 as sqlite
import threading
from queue import Queue, Empty
from . import io
from .path import Path
class Currency:
all = []
by_code = {}
by_name = {}
rates_db = None
def __new__(cls, code=None, name=None):
"""Returns the currency with the given code."""
assert (code is None and name is not None) or (code is not None and name is None)
if code is not None:
try:
return cls.by_code[code]
except KeyError:
raise ValueError('Unknown currency code: %r' % code)
else:
try:
return cls.by_name[name]
except KeyError:
raise ValueError('Unknown currency name: %r' % name)
def __getnewargs__(self):
return (self.code,)
def __getstate__(self):
return None
def __setstate__(self, state):
pass
def __repr__(self):
return '<Currency %s>' % self.code
@staticmethod
def register(code, name, exponent=2, start_date=None, start_rate=1, stop_date=None, latest_rate=1):
"""Registers a new currency and returns it."""
assert code not in Currency.by_code
assert name not in Currency.by_name
currency = object.__new__(Currency)
currency.code = code
currency.name = name
currency.exponent = exponent
currency.start_date = start_date
currency.start_rate = start_rate
currency.stop_date = stop_date
currency.latest_rate = latest_rate
Currency.by_code[code] = currency
Currency.by_name[name] = currency
Currency.all.append(currency)
return currency
@staticmethod
def set_rates_db(db):
Currency.rates_db = db
@staticmethod
def get_rates_db():
if Currency.rates_db is None:
Currency.rates_db = RatesDB() # Make sure we always have some db to work with
return Currency.rates_db
def rates_date_range(self):
"""Returns the range of date for which rates are available for this currency."""
return self.get_rates_db().date_range(self.code)
def value_in(self, currency, date):
"""Returns the value of this currency in terms of the other currency on the given date."""
if self.start_date is not None and date < self.start_date:
return self.start_rate
elif self.stop_date is not None and date > self.stop_date:
return self.latest_rate
else:
return self.get_rates_db().get_rate(date, self.code, currency.code)
def set_CAD_value(self, value, date):
"""Sets the currency's value in CAD on the given date."""
self.get_rates_db().set_CAD_value(date, self.code, value)
BUILTIN_CURRENCIES = {
# In order we want to list them
Currency.register('USD', 'U.S. dollar',
start_date=date(1998, 1, 2), start_rate=1.425, latest_rate=1.0128),
Currency.register('EUR', 'European Euro',
start_date=date(1999, 1, 4), start_rate=1.8123, latest_rate=1.3298),
Currency.register('GBP', 'U.K. pound sterling',
start_date=date(1998, 1, 2), start_rate=2.3397, latest_rate=1.5349),
Currency.register('CAD', 'Canadian dollar',
latest_rate=1),
Currency.register('AUD', 'Australian dollar',
start_date=date(1998, 1, 2), start_rate=0.9267, latest_rate=0.9336),
Currency.register('JPY', 'Japanese yen',
exponent=0, start_date=date(1998, 1, 2), start_rate=0.01076, latest_rate=0.01076),
Currency.register('INR', 'Indian rupee',
start_date=date(1998, 1, 2), start_rate=0.03627, latest_rate=0.02273),
Currency.register('NZD', 'New Zealand dollar',
start_date=date(1998, 1, 2), start_rate=0.8225, latest_rate=0.7257),
Currency.register('CHF', 'Swiss franc',
start_date=date(1998, 1, 2), start_rate=0.9717, latest_rate=0.9273),
Currency.register('ZAR', 'South African rand',
start_date=date(1998, 1, 2), start_rate=0.292, latest_rate=0.1353),
# The rest, alphabetical
Currency.register('AED', 'U.A.E. dirham',
start_date=date(2007, 9, 4), start_rate=0.2858, latest_rate=0.2757),
Currency.register('ANG', 'Neth. Antilles florin',
start_date=date(1998, 1, 2), start_rate=0.7961, latest_rate=0.5722),
Currency.register('ARS', 'Argentine peso',
start_date=date(1998, 1, 2), start_rate=1.4259, latest_rate=0.2589),
Currency.register('ATS', 'Austrian schilling',
start_date=date(1998, 1, 2), start_rate=0.1123, stop_date=date(2001, 12, 31), latest_rate=0.10309), # obsolete (euro)
Currency.register('BBD', 'Barbadian dollar',
start_date=date(2010, 4, 30), start_rate=0.5003, latest_rate=0.5003),
Currency.register('BEF', 'Belgian franc',
start_date=date(1998, 1, 2), start_rate=0.03832, stop_date=date(2001, 12, 31), latest_rate=0.03516), # obsolete (euro)
Currency.register('BHD', 'Bahraini dinar',
exponent=3, start_date=date(2008, 11, 8), start_rate=3.1518, latest_rate=2.6603),
Currency.register('BRL', 'Brazilian real',
start_date=date(1998, 1, 2), start_rate=1.2707, latest_rate=0.5741),
Currency.register('BSD', 'Bahamian dollar',
start_date=date(1998, 1, 2), start_rate=1.425, latest_rate=1.0128),
Currency.register('CLP', 'Chilean peso',
exponent=0, start_date=date(1998, 1, 2), start_rate=0.003236, latest_rate=0.001923),
Currency.register('CNY', 'Chinese renminbi',
start_date=date(1998, 1, 2), start_rate=0.1721, latest_rate=0.1484),
Currency.register('COP', 'Colombian peso',
start_date=date(1998, 1, 2), start_rate=0.00109, latest_rate=0.000513),
Currency.register('CZK', 'Czech Republic koruna',
start_date=date(1998, 2, 2), start_rate=0.04154, latest_rate=0.05202),
Currency.register('DEM', 'German deutsche mark',
start_date=date(1998, 1, 2), start_rate=0.7904, stop_date=date(2001, 12, 31), latest_rate=0.7253), # obsolete (euro)
Currency.register('DKK', 'Danish krone',
start_date=date(1998, 1, 2), start_rate=0.2075, latest_rate=0.1785),
Currency.register('EGP', 'Egyptian Pound',
start_date=date(2008, 11, 27), start_rate=0.2232, latest_rate=0.1805),
Currency.register('ESP', 'Spanish peseta',
exponent=0, start_date=date(1998, 1, 2), start_rate=0.009334, stop_date=date(2001, 12, 31), latest_rate=0.008526), # obsolete (euro)
Currency.register('FIM', 'Finnish markka',
start_date=date(1998, 1, 2), start_rate=0.2611, stop_date=date(2001, 12, 31), latest_rate=0.2386), # obsolete (euro)
Currency.register('FJD', 'Fiji dollar',
start_date=date(1998, 1, 2), start_rate=0.9198, latest_rate=0.5235),
Currency.register('FRF', 'French franc',
start_date=date(1998, 1, 2), start_rate=0.2362, stop_date=date(2001, 12, 31), latest_rate=0.2163), # obsolete (euro)
Currency.register('GHC', 'Ghanaian cedi (old)',
start_date=date(1998, 1, 2), start_rate=0.00063, stop_date=date(2007, 6, 29), latest_rate=0.000115), # obsolete
Currency.register('GHS', 'Ghanaian cedi',
start_date=date(2007, 7, 3), start_rate=1.1397, latest_rate=0.7134),
Currency.register('GRD', 'Greek drachma',
start_date=date(1998, 1, 2), start_rate=0.005, stop_date=date(2001, 12, 31), latest_rate=0.004163), # obsolete (euro)
Currency.register('GTQ', 'Guatemalan quetzal',
start_date=date(2004, 12, 21), start_rate=0.15762, latest_rate=0.1264),
Currency.register('HKD', 'Hong Kong dollar',
start_date=date(1998, 1, 2), start_rate=0.1838, latest_rate=0.130385),
Currency.register('HNL', 'Honduran lempira',
start_date=date(1998, 1, 2), start_rate=0.108, latest_rate=0.0536),
Currency.register('HRK', 'Croatian kuna',
start_date=date(2002, 3, 1), start_rate=0.1863, latest_rate=0.1837),
Currency.register('HUF', 'Hungarian forint',
start_date=date(1998, 2, 2), start_rate=0.007003, latest_rate=0.00493),
Currency.register('IDR', 'Indonesian rupiah',
start_date=date(1998, 2, 2), start_rate=0.000145, latest_rate=0.000112),
Currency.register('IEP', 'Irish pound',
start_date=date(1998, 1, 2), start_rate=2.0235, stop_date=date(2001, 12, 31), latest_rate=1.8012), # obsolete (euro)
Currency.register('ILS', 'Israeli new shekel',
start_date=date(1998, 1, 2), start_rate=0.4021, latest_rate=0.2706),
Currency.register('ISK', 'Icelandic krona',
exponent=0, start_date=date(1998, 1, 2), start_rate=0.01962, latest_rate=0.00782),
Currency.register('ITL', 'Italian lira',
exponent=0, start_date=date(1998, 1, 2), start_rate=0.000804, stop_date=date(2001, 12, 31), latest_rate=0.000733), # obsolete (euro)
Currency.register('JMD', 'Jamaican dollar',
start_date=date(2001, 6, 25), start_rate=0.03341, latest_rate=0.01145),
Currency.register('KRW', 'South Korean won',
exponent=0, start_date=date(1998, 1, 2), start_rate=0.000841, latest_rate=0.000905),
Currency.register('LKR', 'Sri Lanka rupee',
start_date=date(1998, 1, 2), start_rate=0.02304, latest_rate=0.0089),
Currency.register('LTL', 'Lithuanian litas',
start_date=date(2010, 4, 29), start_rate=0.384, latest_rate=0.384),
Currency.register('LVL', 'Latvian lats',
start_date=date(2011, 2, 6), start_rate=1.9136, latest_rate=1.9136),
Currency.register('MAD', 'Moroccan dirham',
start_date=date(1998, 1, 2), start_rate=0.1461, latest_rate=0.1195),
Currency.register('MMK', 'Myanmar (Burma) kyat',
start_date=date(1998, 1, 2), start_rate=0.226, latest_rate=0.1793),
Currency.register('MXN', 'Mexican peso',
start_date=date(1998, 1, 2), start_rate=0.1769, latest_rate=0.08156),
Currency.register('MYR', 'Malaysian ringgit',
start_date=date(1998, 1, 2), start_rate=0.3594, latest_rate=0.3149),
# MZN in not supported in any of my sources, so I'm just creating it with a fixed rate.
Currency.register('MZN', 'Mozambican metical',
start_date=date(2011, 2, 6), start_rate=0.03, stop_date=date(2011, 2, 5), latest_rate=0.03),
Currency.register('NIO', 'Nicaraguan córdoba',
start_date=date(2011, 10, 12), start_rate=0.0448, latest_rate=0.0448),
Currency.register('NLG', 'Netherlands guilder',
start_date=date(1998, 1, 2), start_rate=0.7013, stop_date=date(2001, 12, 31), latest_rate=0.6437), # obsolete (euro)
Currency.register('NOK', 'Norwegian krone',
start_date=date(1998, 1, 2), start_rate=0.1934, latest_rate=0.1689),
Currency.register('PAB', 'Panamanian balboa',
start_date=date(1998, 1, 2), start_rate=1.425, latest_rate=1.0128),
Currency.register('PEN', 'Peruvian new sol',
start_date=date(1998, 1, 2), start_rate=0.5234, latest_rate=0.3558),
Currency.register('PHP', 'Philippine peso',
start_date=date(1998, 1, 2), start_rate=0.0345, latest_rate=0.02262),
Currency.register('PKR', 'Pakistan rupee',
start_date=date(1998, 1, 2), start_rate=0.03238, latest_rate=0.01206),
Currency.register('PLN', 'Polish zloty',
start_date=date(1998, 2, 2), start_rate=0.4108, latest_rate=0.3382),
Currency.register('PTE', 'Portuguese escudo',
exponent=0, start_date=date(1998, 1, 2), start_rate=0.007726, stop_date=date(2001, 12, 31), latest_rate=0.007076), # obsolete (euro)
Currency.register('RON', 'Romanian new leu',
start_date=date(2007, 9, 4), start_rate=0.4314, latest_rate=0.3215),
Currency.register('RSD', 'Serbian dinar',
start_date=date(2007, 9, 4), start_rate=0.0179, latest_rate=0.01338),
Currency.register('RUB', 'Russian rouble',
start_date=date(1998, 1, 2), start_rate=0.2375, latest_rate=0.03443),
Currency.register('SAR', 'Saudi riyal',
start_date=date(2012, 9, 13), start_rate=0.26, latest_rate=0.26),
Currency.register('SEK', 'Swedish krona',
start_date=date(1998, 1, 2), start_rate=0.1787, latest_rate=0.1378),
Currency.register('SGD', 'Singapore dollar',
start_date=date(1998, 1, 2), start_rate=0.84, latest_rate=0.7358),
Currency.register('SIT', 'Slovenian tolar',
start_date=date(2002, 3, 1), start_rate=0.006174, stop_date=date(2006, 12, 29), latest_rate=0.006419), # obsolete (euro)
Currency.register('SKK', 'Slovak koruna',
start_date=date(2002, 3, 1), start_rate=0.03308, stop_date=date(2008, 12, 31), latest_rate=0.05661), # obsolete (euro)
Currency.register('THB', 'Thai baht',
start_date=date(1998, 1, 2), start_rate=0.0296, latest_rate=0.03134),
Currency.register('TND', 'Tunisian dinar',
exponent=3, start_date=date(1998, 1, 2), start_rate=1.2372, latest_rate=0.7037),
Currency.register('TRL', 'Turkish lira',
exponent=0, start_date=date(1998, 1, 2), start_rate=7.0e-06, stop_date=date(2004, 12, 31), latest_rate=8.925e-07), # obsolete
Currency.register('TWD', 'Taiwanese new dollar',
start_date=date(1998, 1, 2), start_rate=0.04338, latest_rate=0.03218),
Currency.register('UAH', 'Ukrainian hryvnia',
start_date=date(2010, 4, 29), start_rate=0.1266, latest_rate=0.1266),
Currency.register('VEB', 'Venezuelan bolivar',
exponent=0, start_date=date(1998, 1, 2), start_rate=0.002827, stop_date=date(2007, 12, 31), latest_rate=0.00046), # obsolete
Currency.register('VEF', 'Venezuelan bolivar fuerte',
start_date=date(2008, 1, 2), start_rate=0.4623, latest_rate=0.2358),
Currency.register('VND', 'Vietnamese dong',
start_date=date(2004, 1, 1), start_rate=8.2e-05, latest_rate=5.3e-05),
Currency.register('XAF', 'CFA franc',
exponent=0, start_date=date(1998, 1, 2), start_rate=0.002362, latest_rate=0.002027),
Currency.register('XCD', 'East Caribbean dollar',
start_date=date(1998, 1, 2), start_rate=0.5278, latest_rate=0.3793),
Currency.register('XPF', 'CFP franc',
exponent=0, start_date=date(1998, 1, 2), start_rate=0.01299, latest_rate=0.01114),
}
BUILTIN_CURRENCY_CODES = {c.code for c in BUILTIN_CURRENCIES}
# For legacy purpose, we need to maintain these global variables
CAD = Currency(code='CAD')
USD = Currency(code='USD')
EUR = Currency(code='EUR')
class CurrencyNotSupportedException(Exception):
"""The current exchange rate provider doesn't support the requested currency."""
class RatesDB:
"""Stores exchange rates for currencies.
The currencies are identified with ISO 4217 code (USD, CAD, EUR, etc.).
The rates are represented as float and represent the value of the currency in CAD.
"""
def __init__(self, db_or_path=':memory:', async=True):
self._cache = {} # {(date, currency): CAD value
self.db_or_path = db_or_path
if isinstance(db_or_path, (str, Path)):
self.con = sqlite.connect(str(db_or_path))
else:
self.con = db_or_path
self._execute("select * from rates where 1=2")
self._rate_providers = []
self.async = async
self._fetched_values = Queue()
self._fetched_ranges = {} # a currency --> (start, end) map
def _execute(self, *args, **kwargs):
def create_tables():
# date is stored as a TEXT YYYYMMDD
sql = "create table rates(date TEXT, currency TEXT, rate REAL NOT NULL)"
self.con.execute(sql)
sql = "create unique index idx_rate on rates (date, currency)"
self.con.execute(sql)
try:
return self.con.execute(*args, **kwargs)
except sqlite.OperationalError: # new db, or other problems
try:
create_tables()
except Exception:
logging.warning("Messy problems with the currency db, starting anew with a memory db")
self.con = sqlite.connect(':memory:')
create_tables()
except sqlite.DatabaseError: # corrupt db
logging.warning("Corrupt currency database at {0}. Starting over.".format(repr(self.db_or_path)))
if isinstance(self.db_or_path, (str, Path)):
self.con.close()
io.remove(Path(self.db_or_path))
self.con = sqlite.connect(str(self.db_or_path))
else:
logging.warning("Can't re-use the file, using a memory table")
self.con = sqlite.connect(':memory:')
create_tables()
return self.con.execute(*args, **kwargs) # try again
def _seek_value_in_CAD(self, str_date, currency_code):
if currency_code == 'CAD':
return 1
def seek(date_op, desc):
sql = "select rate from rates where date %s ? and currency = ? order by date %s limit 1" % (date_op, desc)
cur = self._execute(sql, [str_date, currency_code])
row = cur.fetchone()
if row:
return row[0]
return seek('<=', 'desc') or seek('>=', '') or Currency(currency_code).latest_rate
def _save_fetched_rates(self):
while True:
try:
rates, currency = self._fetched_values.get_nowait()
for rate_date, rate in rates:
self.set_CAD_value(rate_date, currency, rate)
except Empty:
break
def clear_cache(self):
self._cache = {}
def date_range(self, currency_code):
"""Returns (start, end) of the cached rates for currency"""
sql = "select min(date), max(date) from rates where currency = '%s'" % currency_code
cur = self._execute(sql)
start, end = cur.fetchone()
if start and end:
convert = lambda s: datetime.strptime(s, '%Y%m%d').date()
return convert(start), convert(end)
else:
return None
def get_rate(self, 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.
"""
# We want to check self._fetched_values for rates to add.
if not self._fetched_values.empty():
self._save_fetched_rates()
# This method is a bottleneck and has been optimized for speed.
value1 = None
value2 = None
if currency1_code == 'CAD':
value1 = 1
else:
value1 = self._cache.get((date, currency1_code))
if currency2_code == 'CAD':
value2 = 1
else:
value2 = self._cache.get((date, currency2_code))
if value1 is None or value2 is None:
str_date = '%d%02d%02d' % (date.year, date.month, date.day)
if value1 is None:
value1 = self._seek_value_in_CAD(str_date, currency1_code)
self._cache[(date, currency1_code)] = value1
if value2 is None:
value2 = self._seek_value_in_CAD(str_date, currency2_code)
self._cache[(date, currency2_code)] = value2
return value1 / value2
def set_CAD_value(self, date, currency_code, value):
"""Sets the daily value in CAD for currency at date"""
# we must clear the whole cache because there might be other dates affected by this change
# (dates when the currency server has no rates).
self.clear_cache()
str_date = '%d%02d%02d' % (date.year, date.month, date.day)
sql = "replace into rates(date, currency, rate) values(?, ?, ?)"
self._execute(sql, [str_date, currency_code, value])
self.con.commit()
def register_rate_provider(self, rate_provider):
"""Adds `rate_provider` to the list of providers supported by this DB.
A provider if a function(currency, start_date, end_date) that returns a list of
(rate_date, float_rate) as a result. This function will be called asyncronously, so it's ok
if it takes a long time to return.
The rates returned must be the value of 1 `currency` in CAD (Canadian Dollars) at the
specified date.
The provider can be asked for any currency. If it doesn't support it, it has to raise
CurrencyNotSupportedException.
If we support the currency but that there is no rate available for the specified range,
simply return an empty list or None.
"""
self._rate_providers.append(rate_provider)
def ensure_rates(self, start_date, currencies):
"""Ensures that the DB has all the rates it needs for 'currencies' between 'start_date' and today
If there is any rate missing, a request will be made to the currency server. The requests
are made asynchronously.
"""
def do():
for currency, fetch_start, fetch_end in currencies_and_range:
for rate_provider in self._rate_providers:
try:
values = rate_provider(currency, fetch_start, fetch_end)
except CurrencyNotSupportedException:
continue
else:
if values:
self._fetched_values.put((values, currency))
currencies_and_range = []
for currency in currencies:
if currency == 'CAD':
continue
try:
cached_range = self._fetched_ranges[currency]
except KeyError:
cached_range = self.date_range(currency)
range_start = start_date
range_end = date.today()
if cached_range is not None:
cached_start, cached_end = cached_range
if range_start >= cached_start:
# Make a forward fetch
range_start = cached_end + timedelta(days=1)
else:
# Make a backward fetch
range_end = cached_start - timedelta(days=1)
if range_start <= range_end:
currencies_and_range.append((currency, range_start, range_end))
self._fetched_ranges[currency] = (start_date, date.today())
if self.async:
threading.Thread(target=do).start()
else:
do()

22
hscommon/debug.py Normal file
View File

@ -0,0 +1,22 @@
# Created By: Virgil Dupras
# Created On: 2011-04-19
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
import sys
import traceback
# Taken from http://bzimmer.ziclix.com/2008/12/17/python-thread-dumps/
def stacktraces():
code = []
for threadId, stack in sys._current_frames().items():
code.append("\n# ThreadID: %s" % threadId)
for filename, lineno, name, line in traceback.extract_stack(stack):
code.append('File: "%s", line %d, in %s' % (filename, lineno, name))
if line:
code.append(" %s" % (line.strip()))
return "\n".join(code)

25
hscommon/docs/build.rst Normal file
View File

@ -0,0 +1,25 @@
==========================================
: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.

194
hscommon/docs/conf.py Normal file
View File

@ -0,0 +1,194 @@
# -*- 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

@ -0,0 +1,27 @@
===================================================
: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

@ -0,0 +1,62 @@
===================================
: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.

32
hscommon/docs/index.rst Normal file
View File

@ -0,0 +1,32 @@
==============================================
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

26
hscommon/docs/notify.rst Normal file
View File

@ -0,0 +1,26 @@
==========================================
: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.

13
hscommon/docs/path.rst Normal file
View File

@ -0,0 +1,13 @@
========================================
: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.

25
hscommon/docs/reg.rst Normal file
View File

@ -0,0 +1,25 @@
========================================
: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`.

9
hscommon/docs/sqlite.rst Normal file
View File

@ -0,0 +1,9 @@
==========================================
: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.

88
hscommon/docs/util.rst Normal file
View File

@ -0,0 +1,88 @@
========================================
: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.

218
hscommon/geometry.py Normal file
View File

@ -0,0 +1,218 @@
# Created By: Virgil Dupras
# Created On: 2011-08-05
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
from sys import maxsize as INF
from math import sqrt
VERY_SMALL = 0.0000001
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return '<Point {:2.2f}, {:2.2f}>'.format(*self)
def __iter__(self):
yield self.x
yield self.y
def distance_to(self, other):
return Line(self, other).length()
class Line:
def __init__(self, p1, p2):
self.p1 = p1
self.p2 = p2
def __repr__(self):
return '<Line {}, {}>'.format(*self)
def __iter__(self):
yield self.p1
yield self.p2
def dx(self):
return self.p2.x - self.p1.x
def dy(self):
return self.p2.y - self.p1.y
def length(self):
return sqrt(self.dx() ** 2 + self.dy() ** 2)
def slope(self):
if self.dx() == 0:
return INF if self.dy() > 0 else -INF
else:
return self.dy() / self.dx()
def intersection_point(self, other):
# with help from http://paulbourke.net/geometry/lineline2d/
if abs(self.slope() - other.slope()) < VERY_SMALL:
# parallel. Even if coincident, we return nothing
return None
A, B = self
C, D = other
denom = (D.y-C.y) * (B.x-A.x) - (D.x-C.x) * (B.y-A.y)
if denom == 0:
return None
numera = (D.x-C.x) * (A.y-C.y) - (D.y-C.y) * (A.x-C.x)
numerb = (B.x-A.x) * (A.y-C.y) - (B.y-A.y) * (A.x-C.x)
mua = numera / denom;
mub = numerb / denom;
if (0 <= mua <= 1) and (0 <= mub <= 1):
x = A.x + mua * (B.x - A.x)
y = A.y + mua * (B.y - A.y)
return Point(x, y)
else:
return None
class Rect:
def __init__(self, x, y, w, h):
self.x = x
self.y = y
self.w = w
self.h = h
def __iter__(self):
yield self.x
yield self.y
yield self.w
yield self.h
def __repr__(self):
return '<Rect {:2.2f}, {:2.2f}, {:2.2f}, {:2.2f}>'.format(*self)
@classmethod
def from_center(cls, center, width, height):
x = center.x - width / 2
y = center.y - height / 2
return cls(x, y, width, height)
@classmethod
def from_corners(cls, pt1, pt2):
x1, y1 = pt1
x2, y2 = pt2
return cls(min(x1, x2), min(y1, y2), abs(x1-x2), abs(y1-y2))
def center(self):
return Point(self.x + self.w/2, self.y + self.h/2)
def contains_point(self, point):
x, y = point
(x1, y1), (x2, y2) = self.corners()
return (x1 <= x <= x2) and (y1 <= y <= y2)
def contains_rect(self, rect):
pt1, pt2 = rect.corners()
return self.contains_point(pt1) and self.contains_point(pt2)
def corners(self):
return Point(self.x, self.y), Point(self.x+self.w, self.y+self.h)
def intersects(self, other):
r1pt1, r1pt2 = self.corners()
r2pt1, r2pt2 = other.corners()
if r1pt1.x < r2pt1.x:
xinter = r1pt2.x >= r2pt1.x
else:
xinter = r2pt2.x >= r1pt1.x
if not xinter:
return False
if r1pt1.y < r2pt1.y:
yinter = r1pt2.y >= r2pt1.y
else:
yinter = r2pt2.y >= r1pt1.y
return yinter
def lines(self):
pt1, pt4 = self.corners()
pt2 = Point(pt4.x, pt1.y)
pt3 = Point(pt1.x, pt4.y)
l1 = Line(pt1, pt2)
l2 = Line(pt2, pt4)
l3 = Line(pt3, pt4)
l4 = Line(pt1, pt3)
return l1, l2, l3, l4
def scaled_rect(self, dx, dy):
"""Returns a rect that has the same borders at self, but grown/shrunk by dx/dy on each side.
"""
x, y, w, h = self
x -= dx
y -= dy
w += dx * 2
h += dy * 2
return Rect(x, y, w, h)
def united(self, other):
"""Returns the bounding rectangle of this rectangle and `other`.
"""
# ul=upper left lr=lower right
ulcorner1, lrcorner1 = self.corners()
ulcorner2, lrcorner2 = other.corners()
corner1 = Point(min(ulcorner1.x, ulcorner2.x), min(ulcorner1.y, ulcorner2.y))
corner2 = Point(max(lrcorner1.x, lrcorner2.x), max(lrcorner1.y, lrcorner2.y))
return Rect.from_corners(corner1, corner2)
#--- Properties
@property
def top(self):
return self.y
@top.setter
def top(self, value):
self.y = value
@property
def bottom(self):
return self.y + self.h
@bottom.setter
def bottom(self, value):
self.y = value - self.h
@property
def left(self):
return self.x
@left.setter
def left(self, value):
self.x = value
@property
def right(self):
return self.x + self.w
@right.setter
def right(self, value):
self.x = value - self.w
@property
def width(self):
return self.w
@width.setter
def width(self, value):
self.w = value
@property
def height(self):
return self.h
@height.setter
def height(self, value):
self.h = value

0
hscommon/gui/__init__.py Normal file
View File

58
hscommon/gui/base.py Normal file
View File

@ -0,0 +1,58 @@
# Created By: Virgil Dupras
# Created On: 2011/09/09
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
def noop(*args, **kwargs):
pass
class NoopGUI:
def __getattr__(self, func_name):
return noop
# A GUIObject is a cross-toolkit "model" representation of a GUI layer object, for example, a table.
# It acts as a cross-toolkit interface to multiple what we call here a "view". That view is a
# toolkit-specific controller to the actual view (an NSTableView, a QTableView, etc.).
# In our GUIObject, we need a reference to that toolkit-specific controller because some actions,
# have effects on it (for example, prompting it to refresh its data). The GUIObject is typically
# instantiated before its "view", that is why we set it as None on init. However, the GUI
# layer is supposed to set the view as soon as its toolkit-specific controller is instantiated.
# When you subclass GUIObject, you will likely want to update its view on instantiation. That
# is why we call self.view.refresh() in _view_updated(). If you need another type of action on
# view instantiation, just override the method.
class GUIObject:
def __init__(self):
self._view = None
def _view_updated(self):
pass #virtual
def has_view(self):
return (self._view is not None) and (not isinstance(self._view, NoopGUI))
@property
def view(self):
return self._view
@view.setter
def view(self, value):
# There's two times at which we set the view property: On initialization, where we set the
# view that we'll use for your lifetime, and just before the view is deallocated. We need
# to unset our view at that time to avoid calls to a deallocated instance (which means a
# crash).
if self._view is None:
# Initial view assignment
if value is None:
return
self._view = value
self._view_updated()
else:
assert value is None
# Instead of None, we put a NoopGUI() there to avoid rogue view callback raising an
# exception.
self._view = NoopGUI()

160
hscommon/gui/column.py Normal file
View File

@ -0,0 +1,160 @@
# Created By: Virgil Dupras
# Created On: 2010-07-25
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
import copy
from .base import GUIObject
class Column:
def __init__(self, name, display='', visible=True, optional=False):
self.name = name
self.logical_index = 0
self.ordered_index = 0
self.width = 0
self.default_width = 0
self.display = display
self.visible = visible
self.default_visible = visible
self.optional = optional
class Columns(GUIObject):
def __init__(self, table, prefaccess=None, savename=None):
GUIObject.__init__(self)
self.table = table
self.prefaccess = prefaccess
self.savename = savename
# We use copy here for test isolation. If we don't, changing a column affects all tests.
self.column_list = list(map(copy.copy, table.COLUMNS))
for i, column in enumerate(self.column_list):
column.logical_index = i
column.ordered_index = i
self.coldata = {col.name: col for col in self.column_list}
#--- Private
def _get_colname_attr(self, colname, attrname, default):
try:
return getattr(self.coldata[colname], attrname)
except KeyError:
return default
def _set_colname_attr(self, colname, attrname, value):
try:
col = self.coldata[colname]
setattr(col, attrname, value)
except KeyError:
pass
def _optional_columns(self):
return [c for c in self.column_list if c.optional]
#--- Override
def _view_updated(self):
self.restore_columns()
#--- Public
def column_by_index(self, index):
return self.column_list[index]
def column_by_name(self, name):
return self.coldata[name]
def columns_count(self):
return len(self.column_list)
def column_display(self, colname):
return self._get_colname_attr(colname, 'display', '')
def column_is_visible(self, colname):
return self._get_colname_attr(colname, 'visible', True)
def column_width(self, colname):
return self._get_colname_attr(colname, 'width', 0)
def columns_to_right(self, colname):
column = self.coldata[colname]
index = column.ordered_index
return [col.name for col in self.column_list if (col.visible and col.ordered_index > index)]
def menu_items(self):
# Returns a list of (display_name, marked) items for each optional column in the current
# view (marked means that it's visible).
return [(c.display, c.visible) for c in self._optional_columns()]
def move_column(self, colname, index):
colnames = self.colnames
colnames.remove(colname)
colnames.insert(index, colname)
self.set_column_order(colnames)
def reset_to_defaults(self):
self.set_column_order([col.name for col in self.column_list])
for col in self._optional_columns():
col.visible = col.default_visible
col.width = col.default_width
self.view.restore_columns()
def resize_column(self, colname, newwidth):
self._set_colname_attr(colname, 'width', newwidth)
def restore_columns(self):
if not (self.prefaccess and self.savename and self.coldata):
if (not self.savename) and (self.coldata):
# This is a table that will not have its coldata saved/restored. we should
# "restore" its default column attributes.
self.view.restore_columns()
return
for col in self.column_list:
pref_name = '{}.Columns.{}'.format(self.savename, col.name)
coldata = self.prefaccess.get_default(pref_name, fallback_value={})
if 'index' in coldata:
col.ordered_index = coldata['index']
if 'width' in coldata:
col.width = coldata['width']
if col.optional and 'visible' in coldata:
col.visible = coldata['visible']
self.view.restore_columns()
def save_columns(self):
if not (self.prefaccess and self.savename and self.coldata):
return
for col in self.column_list:
pref_name = '{}.Columns.{}'.format(self.savename, col.name)
coldata = {'index': col.ordered_index, 'width': col.width}
if col.optional:
coldata['visible'] = col.visible
self.prefaccess.set_default(pref_name, coldata)
def set_column_order(self, colnames):
colnames = (name for name in colnames if name in self.coldata)
for i, colname in enumerate(colnames):
col = self.coldata[colname]
col.ordered_index = i
def set_column_visible(self, colname, visible):
self.table.save_edits() # the table on the GUI side will stop editing when the columns change
self._set_colname_attr(colname, 'visible', visible)
self.view.set_column_visible(colname, visible)
def set_default_width(self, colname, width):
self._set_colname_attr(colname, 'default_width', width)
def toggle_menu_item(self, index):
col = self._optional_columns()[index]
self.set_column_visible(col.name, not col.visible)
return col.visible
#--- Properties
@property
def ordered_columns(self):
return [col for col in sorted(self.column_list, key=lambda col: col.ordered_index)]
@property
def colnames(self):
return [col.name for col in self.ordered_columns]

View File

@ -0,0 +1,131 @@
# Created By: Virgil Dupras
# Created On: 2011-09-06
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
from collections import Sequence, MutableSequence
from .base import GUIObject
class Selectable(Sequence):
def __init__(self):
self._selected_indexes = []
#--- Private
def _check_selection_range(self):
if not self:
self._selected_indexes = []
if not self._selected_indexes:
return
self._selected_indexes = [index for index in self._selected_indexes if index < len(self)]
if not self._selected_indexes:
self._selected_indexes = [len(self) - 1]
#--- Virtual
def _update_selection(self):
# Takes the table's selection and does appropriates updates on the view and/or model, when
# appropriate. Common sense would dictate that when the selection doesn't change, we don't
# update anything (and thus don't call _update_selection() at all), but there are cases
# where it's false. For example, if our list updates its items but doesn't change its
# selection, we probably want to update the model's selection. A redesign of how this whole
# thing works is probably in order, but not now, there's too much breakage at once involved.
pass
#--- Public
def select(self, indexes):
if isinstance(indexes, int):
indexes = [indexes]
self.selected_indexes = indexes
self._update_selection()
#--- Properties
@property
def selected_index(self):
return self._selected_indexes[0] if self._selected_indexes else None
@selected_index.setter
def selected_index(self, value):
self.selected_indexes = [value]
@property
def selected_indexes(self):
return self._selected_indexes
@selected_indexes.setter
def selected_indexes(self, value):
self._selected_indexes = value
self._selected_indexes.sort()
self._check_selection_range()
class SelectableList(MutableSequence, Selectable):
def __init__(self, items=None):
Selectable.__init__(self)
if items:
self._items = list(items)
else:
self._items = []
def __delitem__(self, key):
self._items.__delitem__(key)
self._check_selection_range()
self._on_change()
def __getitem__(self, key):
return self._items.__getitem__(key)
def __len__(self):
return len(self._items)
def __setitem__(self, key, value):
self._items.__setitem__(key, value)
self._on_change()
#--- Override
def append(self, item):
self._items.append(item)
self._on_change()
def insert(self, index, item):
self._items.insert(index, item)
self._on_change()
def remove(self, row):
self._items.remove(row)
self._check_selection_range()
self._on_change()
#--- Virtual
def _on_change(self):
pass
#--- Public
def search_by_prefix(self, prefix):
prefix = prefix.lower()
for index, s in enumerate(self):
if s.lower().startswith(prefix):
return index
return -1
class GUISelectableList(SelectableList, GUIObject):
#--- View interface
# refresh()
# update_selection()
#
def __init__(self, items=None):
SelectableList.__init__(self, items)
GUIObject.__init__(self)
def _view_updated(self):
self.view.refresh()
def _update_selection(self):
self.view.update_selection()
def _on_change(self):
self.view.refresh()

297
hscommon/gui/table.py Normal file
View File

@ -0,0 +1,297 @@
# Created By: Eric Mc Sween
# Created On: 2008-05-29
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
from collections import MutableSequence, namedtuple
from .base import GUIObject
from .selectable_list import Selectable
# We used to directly subclass list, but it caused problems at some point with deepcopy
# Adding and removing footer here and there might seem (and is) hackish, but it's much simpler than
# the alternative, which is to override magic methods and adjust the results. When we do that, there
# the slice stuff that we have to implement and it gets quite complex.
# Moreover, the most frequent operation on a table is __getitem__, and making checks to know whether
# the key is a header or footer at each call would make that operation, which is the most used,
# slower.
class Table(MutableSequence, Selectable):
def __init__(self):
Selectable.__init__(self)
self._rows = []
self._header = None
self._footer = None
def __delitem__(self, key):
self._rows.__delitem__(key)
if self._header is not None and ((not self) or (self[0] is not self._header)):
self._header = None
if self._footer is not None and ((not self) or (self[-1] is not self._footer)):
self._footer = None
self._check_selection_range()
def __getitem__(self, key):
return self._rows.__getitem__(key)
def __len__(self):
return len(self._rows)
def __setitem__(self, key, value):
self._rows.__setitem__(key, value)
def append(self, item):
if self._footer is not None:
self._rows.insert(-1, item)
else:
self._rows.append(item)
def insert(self, index, item):
if (self._header is not None) and (index == 0):
index = 1
if (self._footer is not None) and (index >= len(self)):
index = len(self) - 1
self._rows.insert(index, item)
def remove(self, row):
if row is self._header:
self._header = None
if row is self._footer:
self._footer = None
self._rows.remove(row)
self._check_selection_range()
def sort_by(self, column_name, desc=False):
if self._header is not None:
self._rows.pop(0)
if self._footer is not None:
self._rows.pop()
key = lambda row: row.sort_key_for_column(column_name)
self._rows.sort(key=key, reverse=desc)
if self._header is not None:
self._rows.insert(0, self._header)
if self._footer is not None:
self._rows.append(self._footer)
#--- Properties
@property
def footer(self):
return self._footer
@footer.setter
def footer(self, value):
if self._footer is not None:
self._rows.pop()
if value is not None:
self._rows.append(value)
self._footer = value
@property
def header(self):
return self._header
@header.setter
def header(self, value):
if self._header is not None:
self._rows.pop(0)
if value is not None:
self._rows.insert(0, value)
self._header = value
@property
def row_count(self):
result = len(self)
if self._footer is not None:
result -= 1
if self._header is not None:
result -= 1
return result
@property
def rows(self):
start = None
end = None
if self._footer is not None:
end = -1
if self._header is not None:
start = 1
return self[start:end]
@property
def selected_row(self):
return self[self.selected_index] if self.selected_index is not None else None
@selected_row.setter
def selected_row(self, value):
try:
self.selected_index = self.index(value)
except ValueError:
pass
@property
def selected_rows(self):
return [self[index] for index in self.selected_indexes]
SortDescriptor = namedtuple('SortDescriptor', 'column desc')
class GUITable(Table, GUIObject):
def __init__(self):
GUIObject.__init__(self)
Table.__init__(self)
self.edited = None
self._sort_descriptor = None
#--- Virtual
def _do_add(self):
# Creates a new row, adds it in the table and returns (row, insert_index)
raise NotImplementedError()
def _do_delete(self):
# Delete the selected rows
pass
def _fill(self):
# Called by refresh()
# Fills the table with all the rows that this table is supposed to have.
pass
def _is_edited_new(self):
return False
def _restore_selection(self, previous_selection):
if not self.selected_indexes:
if previous_selection:
self.select(previous_selection)
else:
self.select([len(self) - 1])
#--- Public
def add(self):
self.view.stop_editing()
if self.edited is not None:
self.save_edits()
row, insert_index = self._do_add()
self.insert(insert_index, row)
self.select([insert_index])
self.edited = row
self.view.refresh()
self.view.start_editing()
def can_edit_cell(self, column_name, row_index):
# A row is, by default, editable as soon as it has an attr with the same name as `column`.
# If can_edit() returns False, the row is not editable at all. You can set editability of
# rows at the attribute level with can_edit_* properties
row = self[row_index]
return row.can_edit_cell(column_name)
def cancel_edits(self):
if self.edited is None:
return
self.view.stop_editing()
if self._is_edited_new():
previous_selection = self.selected_indexes
self.remove(self.edited)
self._restore_selection(previous_selection)
self._update_selection()
else:
self.edited.load()
self.edited = None
self.view.refresh()
def delete(self):
self.view.stop_editing()
if self.edited is not None:
self.cancel_edits()
return
if self:
self._do_delete()
def refresh(self, refresh_view=True):
self.cancel_edits()
previous_selection = self.selected_indexes
del self[:]
self._fill()
sd = self._sort_descriptor
if sd is not None:
Table.sort_by(self, column_name=sd.column, desc=sd.desc)
self._restore_selection(previous_selection)
if refresh_view:
self.view.refresh()
def save_edits(self):
if self.edited is None:
return
row = self.edited
self.edited = None
row.save()
def sort_by(self, column_name, desc=False):
Table.sort_by(self, column_name=column_name, desc=desc)
self._sort_descriptor = SortDescriptor(column_name, desc)
self._update_selection()
self.view.refresh()
class Row:
def __init__(self, table):
super(Row, self).__init__()
self.table = table
def _edit(self):
if self.table.edited is self:
return
assert self.table.edited is None
self.table.edited = self
#--- Virtual
def can_edit(self):
return True
def load(self):
raise NotImplementedError()
def save(self):
raise NotImplementedError()
def sort_key_for_column(self, column_name):
# Most of the time, the adequate sort key for a column is the column name with '_' prepended
# to it. This member usually corresponds to the unformated version of the column. If it's
# not there, we try the column_name without underscores
# Of course, override for exceptions.
try:
return getattr(self, '_' + column_name)
except AttributeError:
return getattr(self, column_name)
#--- Public
def can_edit_cell(self, column_name):
if not self.can_edit():
return False
# '_' is in case column is a python keyword
if not hasattr(self, column_name):
if hasattr(self, column_name + '_'):
column_name = column_name + '_'
else:
return False
if hasattr(self, 'can_edit_' + column_name):
return getattr(self, 'can_edit_' + column_name)
# If the row has a settable property, we can edit the cell
rowclass = self.__class__
prop = getattr(rowclass, column_name, None)
if prop is None:
return False
return bool(getattr(prop, 'fset', None))
def get_cell_value(self, attrname):
if attrname == 'from':
attrname = 'from_'
return getattr(self, attrname)
def set_cell_value(self, attrname, value):
if attrname == 'from':
attrname = 'from_'
setattr(self, attrname, value)

View File

@ -0,0 +1,55 @@
# Created On: 2012/01/23
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
from .base import GUIObject
from ..util import nonone
class TextField(GUIObject):
def __init__(self):
GUIObject.__init__(self)
self._text = ''
self._value = None
#--- Virtual
def _parse(self, text):
return text
def _format(self, value):
return value
def _update(self, newvalue):
pass
#--- Override
def _view_updated(self):
self.view.refresh()
#--- Public
def refresh(self):
self.view.refresh()
@property
def text(self):
return self._text
@text.setter
def text(self, newtext):
self.value = self._parse(nonone(newtext, ''))
@property
def value(self):
return self._value
@value.setter
def value(self, newvalue):
if newvalue == self._value:
return
self._value = newvalue
self._text = self._format(newvalue)
self._update(self._value)
self.refresh()

166
hscommon/gui/tree.py Normal file
View File

@ -0,0 +1,166 @@
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
from collections import MutableSequence
from .base import GUIObject
class Node(MutableSequence):
def __init__(self, name):
self._name = name
self._parent = None
self._path = None
self._children = []
def __repr__(self):
return '<Node %r>' % self.name
#--- MutableSequence overrides
def __delitem__(self, key):
self._children.__delitem__(key)
def __getitem__(self, key):
return self._children.__getitem__(key)
def __len__(self):
return len(self._children)
def __setitem__(self, key, value):
self._children.__setitem__(key, value)
def append(self, node):
self._children.append(node)
node._parent = self
node._path = None
def insert(self, index, node):
self._children.insert(index, node)
node._parent = self
node._path = None
#--- Public
def clear(self):
del self[:]
def find(self, predicate, include_self=True):
try:
return next(self.findall(predicate, include_self=include_self))
except StopIteration:
return None
def findall(self, predicate, include_self=True):
if include_self and predicate(self):
yield self
for child in self:
for found in child.findall(predicate, include_self=True):
yield found
def get_node(self, index_path):
result = self
if index_path:
for index in index_path:
result = result[index]
return result
def get_path(self, target_node):
if target_node is None:
return None
return target_node.path
@property
def children_count(self):
return len(self)
@property
def name(self):
return self._name
@property
def parent(self):
return self._parent
@property
def path(self):
if self._path is None:
if self._parent is None:
self._path = []
else:
self._path = self._parent.path + [self._parent.index(self)]
return self._path
@property
def root(self):
if self._parent is None:
return self
else:
return self._parent.root
class Tree(Node, GUIObject):
def __init__(self):
Node.__init__(self, '')
GUIObject.__init__(self)
self._selected_nodes = []
#--- Virtual
def _select_nodes(self, nodes):
# all selection changes go through this method, so you can override this if you want to
# customize the tree's behavior.
self._selected_nodes = nodes
#--- Override
def _view_updated(self):
self.view.refresh()
#--- Public
def clear(self):
self._selected_nodes = []
Node.clear(self)
@property
def selected_node(self):
return self._selected_nodes[0] if self._selected_nodes else None
@selected_node.setter
def selected_node(self, node):
if node is not None:
self._select_nodes([node])
else:
self._select_nodes([])
@property
def selected_nodes(self):
return self._selected_nodes
@selected_nodes.setter
def selected_nodes(self, nodes):
self._select_nodes(nodes)
@property
def selected_path(self):
return self.get_path(self.selected_node)
@selected_path.setter
def selected_path(self, index_path):
if index_path is not None:
self.selected_paths = [index_path]
else:
self._select_nodes([])
@property
def selected_paths(self):
return list(map(self.get_path, self._selected_nodes))
@selected_paths.setter
def selected_paths(self, index_paths):
nodes = []
for path in index_paths:
try:
nodes.append(self.get_node(path))
except IndexError:
pass
self._select_nodes(nodes)

79
hscommon/io.py Normal file
View File

@ -0,0 +1,79 @@
# Created By: Virgil Dupras
# Created On: 2007-10-23
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
# HS code should only deal with Path instances, not string paths. One of the annoyances of this
# is to always have to convert Path instances with unicode() when calling open() or listdir() etc..
# this unit takes care of this
import builtins
import os
import os.path
import shutil
import logging
def log_io_error(func):
""" Catches OSError, IOError and WindowsError and log them
"""
def wrapper(path, *args, **kwargs):
try:
return func(path, *args, **kwargs)
except (IOError, OSError) as e:
msg = 'Error "{0}" during operation "{1}" on "{2}": "{3}"'
classname = e.__class__.__name__
funcname = func.__name__
logging.warn(msg.format(classname, funcname, str(path), str(e)))
return wrapper
def copy(source_path, dest_path):
return shutil.copy(str(source_path), str(dest_path))
def copytree(source_path, dest_path, *args, **kwargs):
return shutil.copytree(str(source_path), str(dest_path), *args, **kwargs)
def exists(path):
return os.path.exists(str(path))
def isdir(path):
return os.path.isdir(str(path))
def isfile(path):
return os.path.isfile(str(path))
def islink(path):
return os.path.islink(str(path))
def listdir(path):
return os.listdir(str(path))
def mkdir(path, *args, **kwargs):
return os.mkdir(str(path), *args, **kwargs)
def makedirs(path, *args, **kwargs):
return os.makedirs(str(path), *args, **kwargs)
def move(source_path, dest_path):
return shutil.move(str(source_path), str(dest_path))
def open(path, *args, **kwargs):
return builtins.open(str(path), *args, **kwargs)
def remove(path):
return os.remove(str(path))
def rename(source_path, dest_path):
return os.rename(str(source_path), str(dest_path))
def rmdir(path):
return os.rmdir(str(path))
def rmtree(path):
return shutil.rmtree(str(path))
def stat(path):
return os.stat(str(path))

177
hscommon/loc.py Normal file
View File

@ -0,0 +1,177 @@
import os
import os.path as op
import shutil
import re
import tempfile
import polib
from . import pygettext
from .util import modified_after, dedupe, ensure_folder, ensure_file
from .build import print_and_do, ensure_empty_folder, copy
LC_MESSAGES = 'LC_MESSAGES'
def get_langs(folder):
return [name for name in os.listdir(folder) if op.isdir(op.join(folder, name))]
def files_with_ext(folder, ext):
return [op.join(folder, fn) for fn in os.listdir(folder) if fn.endswith(ext)]
def generate_pot(folders, outpath, keywords, merge=False):
if merge and not op.exists(outpath):
merge = False
if merge:
_, genpath = tempfile.mkstemp()
else:
genpath = outpath
pyfiles = []
for folder in folders:
for root, dirs, filenames in os.walk(folder):
keep = [fn for fn in filenames if fn.endswith('.py')]
pyfiles += [op.join(root, fn) for fn in keep]
pygettext.main(pyfiles, outpath=genpath, keywords=keywords)
if merge:
merge_po_and_preserve(genpath, outpath)
os.remove(genpath)
def compile_all_po(base_folder):
langs = get_langs(base_folder)
for lang in langs:
pofolder = op.join(base_folder, lang, LC_MESSAGES)
pofiles = files_with_ext(pofolder, '.po')
for pofile in pofiles:
p = polib.pofile(pofile)
p.save_as_mofile(pofile[:-3] + '.mo')
def merge_locale_dir(target, mergeinto):
langs = get_langs(target)
for lang in langs:
if not op.exists(op.join(mergeinto, lang)):
continue
mofolder = op.join(target, lang, LC_MESSAGES)
mofiles = files_with_ext(mofolder, '.mo')
for mofile in mofiles:
shutil.copy(mofile, op.join(mergeinto, lang, LC_MESSAGES))
def merge_pots_into_pos(folder):
# We're going to take all pot files in `folder` and for each lang, merge it with the po file
# with the same name.
potfiles = files_with_ext(folder, '.pot')
for potfile in potfiles:
refpot = polib.pofile(potfile)
refname = op.splitext(op.basename(potfile))[0]
for lang in get_langs(folder):
po = polib.pofile(op.join(folder, lang, LC_MESSAGES, refname + '.po'))
po.merge(refpot)
po.save()
def merge_po_and_preserve(source, dest):
# Merges source entries into dest, but keep old entries intact
sourcepo = polib.pofile(source)
destpo = polib.pofile(dest)
for entry in sourcepo:
if destpo.find(entry.msgid) is not None:
# The entry is already there
continue
destpo.append(entry)
destpo.save()
#--- Cocoa
def all_lproj_paths(folder):
return files_with_ext(folder, '.lproj')
def escape_cocoa_strings(s):
return s.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n')
def unescape_cocoa_strings(s):
return s.replace('\\\\', '\\').replace('\\"', '"').replace('\\n', '\n')
def strings2pot(target, dest):
with open(target, 'rt', encoding='utf-8') as fp:
contents = fp.read()
# We're reading an en.lproj file. We only care about the righthand part of the translation.
re_trans = re.compile(r'".*" = "(.*)";')
strings = re_trans.findall(contents)
ensure_file(dest)
po = polib.pofile(dest)
for s in dedupe(strings):
s = unescape_cocoa_strings(s)
entry = po.find(s)
if entry is None:
entry = polib.POEntry(msgid=s)
po.append(entry)
# we don't know or care about a line number so we put 0
entry.occurrences.append((target, '0'))
entry.occurrences = dedupe(entry.occurrences)
po.save()
def allstrings2pot(lprojpath, dest, excludes=None):
allstrings = files_with_ext(lprojpath, '.strings')
if excludes:
allstrings = [p for p in allstrings if op.splitext(op.basename(p))[0] not in excludes]
for strings_path in allstrings:
strings2pot(strings_path, dest)
def po2strings(pofile, en_strings, dest):
# Takes en_strings and replace all righthand parts of "foo" = "bar"; entries with translations
# in pofile, then puts the result in dest.
po = polib.pofile(pofile)
if not modified_after(pofile, dest):
return
ensure_folder(op.dirname(dest))
print("Creating {} from {}".format(dest, pofile))
with open(en_strings, 'rt', encoding='utf-8') as fp:
contents = fp.read()
re_trans = re.compile(r'(?<= = ").*(?=";\n)')
def repl(match):
s = match.group(0)
unescaped = unescape_cocoa_strings(s)
entry = po.find(unescaped)
if entry is None:
print("WARNING: Could not find entry '{}' in .po file".format(s))
return s
trans = entry.msgstr
return escape_cocoa_strings(trans) if trans else s
contents = re_trans.sub(repl, contents)
with open(dest, 'wt', encoding='utf-8') as fp:
fp.write(contents)
def generate_cocoa_strings_from_code(code_folder, dest_folder):
# Uses the "genstrings" command to generate strings file from all .m files in "code_folder".
# The strings file (their name depends on the localization table used in the source) will be
# placed in "dest_folder".
# genstrings produces utf-16 files with comments. After having generated the files, we convert
# them to utf-8 and remove the comments.
ensure_empty_folder(dest_folder)
print_and_do('genstrings -o "{}" `find "{}" -name *.m | xargs`'.format(dest_folder, code_folder))
for stringsfile in os.listdir(dest_folder):
stringspath = op.join(dest_folder, stringsfile)
with open(stringspath, 'rt', encoding='utf-16') as fp:
content = fp.read()
content = re.sub('/\*.*?\*/', '', content)
content = re.sub('\n{2,}', '\n', content)
# I have no idea why, but genstrings seems to have problems with "%" character in strings
# and inserts (number)$ after it. Find these bogus inserts and remove them.
content = re.sub('%\d\$', '%', content)
with open(stringspath, 'wt', encoding='utf-8') as fp:
fp.write(content)
def build_cocoa_localizations(app, en_stringsfile=op.join('cocoa', 'en.lproj', 'Localizable.strings')):
# Creates .lproj folders with Localizable.strings and cocoalib.strings based on cocoalib.po and
# ui.po for all available languages as well as base strings files in en.lproj. These lproj
# folders are created in `app`'s (a OSXAppStructure) resource folder.
print("Creating lproj folders based on .po files")
en_cocoastringsfile = op.join('cocoalib', 'en.lproj', 'cocoalib.strings')
for lang in get_langs('locale'):
pofile = op.join('locale', lang, 'LC_MESSAGES', 'ui.po')
dest_lproj = op.join(app.resources, lang + '.lproj')
ensure_folder(dest_lproj)
po2strings(pofile, en_stringsfile, op.join(dest_lproj, 'Localizable.strings'))
pofile = op.join('cocoalib', 'locale', lang, 'LC_MESSAGES', 'cocoalib.po')
po2strings(pofile, en_cocoastringsfile, op.join(dest_lproj, 'cocoalib.strings'))
# We also have to copy the "en.lproj" strings
en_lproj = op.join(app.resources, 'en.lproj')
ensure_folder(en_lproj)
copy(en_stringsfile, en_lproj)
copy(en_cocoastringsfile, en_lproj)

View File

@ -0,0 +1,20 @@
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: hscommon\n"
"PO-Revision-Date: 2013-04-28 18:29+0000\n"
"Last-Translator: hsoft <hsoft@hardcoded.net>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n"
"Language: cs\n"
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
#: hscommon/reg.py:32
msgid ""
"{name} is fairware, which means \"open source software developed with expectation of fair contributions from users\". It's a very interesting concept, but one year of fairware has shown that most people just want to know how much it costs and not be bothered with theories about intellectual property.\n"
"\n"
"So I won't bother you and will be very straightforward: You can try {name} for free but you have to buy it in order to use it without limitations. In demo mode, {name} {limitation}.\n"
"\n"
"So it's as simple as this. If you're curious about fairware, however, I encourage you to read more about it by clicking on the \"Fairware?\" button."
msgstr "{name} je fairware, což znamená \"open source software vyvíjený v očekávání poctivých příspěvků od uživatelů\". Jde o velmi zajímavý nápad, a po roce se ukazuje, že většina lidí se zajímá o cenu vývoje, ale jsou jim ukradené povídačky o duševním vlastnictví.\n\nTakže vás nebudu otravovat a řeknu to bez okolků: {name} si můžete zdarma vyzkoušet, ale pokud ho chcete používat bez omezení, musíte si ho koupit. V demo režimu, {name} {limitation}.\n\nA to je celé. Pokud se o fairware chcete dozvědět více, klepněte na tlačítko \"Fairware?\"."

View File

@ -0,0 +1,30 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: utf-8\n"
#: hscommon/reg.py:39
msgid ""
"{name} is Fairware, which means \"open source software developed with expectation of fair contributions from users\". Hours have been invested in this software with the expectation that users will be fair enough to compensate them. The \"Unpaid hours\" figure you see below is the hours that have yet to be compensated for this project.\n"
"\n"
"If you like this application, please make a contribution that you consider fair. Thanks!\n"
"\n"
"If you cannot afford to contribute, you can either ignore this reminder or send an e-mail at support@hardcoded.net so I can send you a registration key.\n"
"\n"
"This dialog doesn't show when there are no unpaid hours or when you have a valid contribution key."
msgstr ""
"{name} ist Fairware, das bedeutet \"Open Source Software, entwickelt in der Hoffnung auf einen fairen Beitrag von den Benutzern\". Viel Zeit wurde in die Software investiert, mit der Erwartung der Nutzer möge fair genug sein die Entwickler für ihren Einsatz zu kompensieren. Die \"Unbezahlte Stunden\" Abbildung zeigt die Anzahl der Stunden die noch nicht bezahlt wurden.\n"
"Wenn Sie diese Anwendung mögen, so spenden Sie bitte einen Ihrer Ansicht nach angemessenen Betrag. Danke!\n"
"\n"
"Wenn Sie es sich nicht leisten können zu spenden, können Sie diese Erinnerung entweder ignorieren oder mir eine Anfrage an hsoft@hardcoded.net schicken, mit der Bitte für einen Registrierungsschlüssel.\n"
"\n"
"Dieser Dialog erscheint nicht, wenn es keine unbezahlten Stunden gibt oder Sie einen gültigen Registrierungsschlüssel besitzen."
#: hscommon/reg.py:51
msgid ""
"{name} is fairware, which means \"open source software developed with expectation of fair contributions from users\". It's a very interesting concept, but one year of fairware has shown that most people just want to know how much it costs and not be bothered with theories about intellectual property.\n"
"\n"
"So I won't bother you and will be very straightforward: You can try {name} for free but you have to buy it in order to use it without limitations. In demo mode, {name} {limitation}.\n"
"\n"
"So it's as simple as this. If you're curious about fairware, however, I encourage you to read more about it by clicking on the \"Fairware?\" button."
msgstr ""

View File

@ -0,0 +1,20 @@
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: hscommon\n"
"PO-Revision-Date: 2013-04-28 18:29+0000\n"
"Last-Translator: hsoft <hsoft@hardcoded.net>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n"
"Language: es\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: hscommon/reg.py:32
msgid ""
"{name} is fairware, which means \"open source software developed with expectation of fair contributions from users\". It's a very interesting concept, but one year of fairware has shown that most people just want to know how much it costs and not be bothered with theories about intellectual property.\n"
"\n"
"So I won't bother you and will be very straightforward: You can try {name} for free but you have to buy it in order to use it without limitations. In demo mode, {name} {limitation}.\n"
"\n"
"So it's as simple as this. If you're curious about fairware, however, I encourage you to read more about it by clicking on the \"Fairware?\" button."
msgstr "{name} es Fairware, es decir \"software de código abierto desarrollado con la expectativa de recibir una retribución justa por parte de los usuarios\". Es una idea muy interesante, aunque tras más de un año de uso es evidente que la mayoría de los usuarios sólo están interesados en el precio del producto y no en teorías sobre la propiedad intelectual.\n\nAsí pues seré claro: puede probar {name} gratuitamente pero debe comprarlo para un uso completo sin limitaciones. En el modo de prueba, {name} {limitation}.\n\nEn resumen, si tiene curiosidad por conocer fairware le animo a que lea sobre ello pulsando el botón de \"¿Fairware?\"."

View File

@ -0,0 +1,20 @@
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: hscommon\n"
"PO-Revision-Date: 2013-04-28 18:29+0000\n"
"Last-Translator: hsoft <hsoft@hardcoded.net>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n"
"Language: fr\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: hscommon/reg.py:32
msgid ""
"{name} is fairware, which means \"open source software developed with expectation of fair contributions from users\". It's a very interesting concept, but one year of fairware has shown that most people just want to know how much it costs and not be bothered with theories about intellectual property.\n"
"\n"
"So I won't bother you and will be very straightforward: You can try {name} for free but you have to buy it in order to use it without limitations. In demo mode, {name} {limitation}.\n"
"\n"
"So it's as simple as this. If you're curious about fairware, however, I encourage you to read more about it by clicking on the \"Fairware?\" button."
msgstr "{name} est Fairware, ce qui signifie \"open source développé avec des attentes de contributions justes de la part des utilisateurs\". C'est un concept excessivement intéressant, mais un an de fairware a révélé que la plupart des gens ne sont que peu intéressés à des discours sur la propriété intellectuelle et veulent simplement savoir combien ça coûte.\n\nDonc, je serai bref et direct: Vous pouvez essayer {name} gratuitement, mais un achat est requis pour un usage sans limitation. En mode démo, {name} {limitation}.\n\nC'est aussi simple que ça. Par contre, si vous êtes curieux, je vous encourage à cliquer sur le bouton \"Fairware?\" pour en savoir plus."

View File

@ -0,0 +1,15 @@
msgid ""
msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: utf-8\n"
#: hscommon/reg.py:32
msgid ""
"{name} is fairware, which means \"open source software developed with expectation of fair contributions from users\". It's a very interesting concept, but one year of fairware has shown that most people just want to know how much it costs and not be bothered with theories about intellectual property.\n"
"\n"
"So I won't bother you and will be very straightforward: You can try {name} for free but you have to buy it in order to use it without limitations. In demo mode, {name} {limitation}.\n"
"\n"
"So it's as simple as this. If you're curious about fairware, however, I encourage you to read more about it by clicking on the \"Fairware?\" button."
msgstr ""

View File

@ -0,0 +1,20 @@
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: hscommon\n"
"PO-Revision-Date: 2013-04-28 18:29+0000\n"
"Last-Translator: hsoft <hsoft@hardcoded.net>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n"
"Language: hy\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: hscommon/reg.py:32
msgid ""
"{name} is fairware, which means \"open source software developed with expectation of fair contributions from users\". It's a very interesting concept, but one year of fairware has shown that most people just want to know how much it costs and not be bothered with theories about intellectual property.\n"
"\n"
"So I won't bother you and will be very straightforward: You can try {name} for free but you have to buy it in order to use it without limitations. In demo mode, {name} {limitation}.\n"
"\n"
"So it's as simple as this. If you're curious about fairware, however, I encourage you to read more about it by clicking on the \"Fairware?\" button."
msgstr "{name}-ը fairware է, ինչը նշանակում է \"ազատ կոդով ծրագիր, որը զարգացվում է՝ ակնկալելով օգտվողների աջակցությունը\": Սա շատ հետաքրքիր սկզբունք է, սակայն մեկ տարվա fairware-ի արդյունքը ցույց է տալիս, որ շատ մարդիկ պարզապես ցանկանում են իմանալ, թե այն ինչ արժե, բայց չեն մտահոգվում ինտելեկտուալ սեփականության մասին:\n\nՈւստի ես չեմ ցանկանում խանգարել Ձեզ և կլինեմ շատ պարզ. Կարող եք փորձել {name}-ը ազատորեն, բայց պետք է գնեք ծրագիրը՝ հանելու համար բոլոր սահմանափակումները: Փորձնական եղանակում {name} {limitation}:\n\nԱմեն ինչ պարզ է, եթե Ձեզ հետաքրքիր է fairware-ը, ապա կարող եք մանրամասն կարդաք՝ սեղմելով այս հղմանը՝ \"Fairware է՞\":"

View File

@ -0,0 +1,20 @@
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: hscommon\n"
"PO-Revision-Date: 2013-04-28 18:29+0000\n"
"Last-Translator: hsoft <hsoft@hardcoded.net>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n"
"Language: it\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: hscommon/reg.py:32
msgid ""
"{name} is fairware, which means \"open source software developed with expectation of fair contributions from users\". It's a very interesting concept, but one year of fairware has shown that most people just want to know how much it costs and not be bothered with theories about intellectual property.\n"
"\n"
"So I won't bother you and will be very straightforward: You can try {name} for free but you have to buy it in order to use it without limitations. In demo mode, {name} {limitation}.\n"
"\n"
"So it's as simple as this. If you're curious about fairware, however, I encourage you to read more about it by clicking on the \"Fairware?\" button."
msgstr "{name} è denominato \"Fairware\", che significa \"software 'open source' sviluppato aspettandosi un contributo equo e corretto da parte degli utilizzatori\". E' un concetto molto interessante, ma un anno di 'fairware' ha dimostrato che la maggior parte della gente vuole solo sapere 'quanto costa' e non essere scocciata con delle teorie sulla proprietà intellettuale.\n\nCosì non vi disturberò oltre e sarò diretto: potete provare {name} gratuitamente ma dovrete acquistarlo per usarlo senza limitazioni. In modalità 'demo', {name} {limitation}.\n\nIn questo modo è semplice. Se siete curiosi e volete approfondire il concetto di 'fairware', vi invito a leggere di più sull'argomento cliccando sul pulsante \"Fairware?\"."

View File

@ -0,0 +1,20 @@
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: hscommon\n"
"PO-Revision-Date: 2013-04-28 18:29+0000\n"
"Last-Translator: hsoft <hsoft@hardcoded.net>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n"
"Language: nl\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: hscommon/reg.py:32
msgid ""
"{name} is fairware, which means \"open source software developed with expectation of fair contributions from users\". It's a very interesting concept, but one year of fairware has shown that most people just want to know how much it costs and not be bothered with theories about intellectual property.\n"
"\n"
"So I won't bother you and will be very straightforward: You can try {name} for free but you have to buy it in order to use it without limitations. In demo mode, {name} {limitation}.\n"
"\n"
"So it's as simple as this. If you're curious about fairware, however, I encourage you to read more about it by clicking on the \"Fairware?\" button."
msgstr "{name} is Fairware, dit betekent \"open source software ontwikkeld in de hoop op een redelijke bijdrage van gebruikers\". Het is een interessant concept, maar na een jaar fairware is gebleken dat de meeste mensen gewoon willen weten wat het kost en niet lastig gevallen willen worden met theorieën over intellectueel eigendom.\n\nIk zal u dus niet lastig vallen en duidelijk zijn: U kunt {name} gratis proberen, maar moet het kopen om het zonder beperkingen te kunnen gebruiken. In demo mode {name} {limitation}.\n\nHet is dus eigenlijk heel simpel. Als u toch geïnteresseerd bent in fairware, raad ik u aan hier meer over te lezen door op de knop \"Fairware?\" te klikken."

View File

@ -0,0 +1,20 @@
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: hscommon\n"
"PO-Revision-Date: 2013-04-28 18:29+0000\n"
"Last-Translator: hsoft <hsoft@hardcoded.net>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n"
"Language: pt_BR\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: hscommon/reg.py:32
msgid ""
"{name} is fairware, which means \"open source software developed with expectation of fair contributions from users\". It's a very interesting concept, but one year of fairware has shown that most people just want to know how much it costs and not be bothered with theories about intellectual property.\n"
"\n"
"So I won't bother you and will be very straightforward: You can try {name} for free but you have to buy it in order to use it without limitations. In demo mode, {name} {limitation}.\n"
"\n"
"So it's as simple as this. If you're curious about fairware, however, I encourage you to read more about it by clicking on the \"Fairware?\" button."
msgstr "{name} é fairware, o que quer dizer \"software de código aberto desenvolvido sob a expectativa de contribuição justa de seus usuários\". É um conceito muito interessante, mas um ano de fairware mostrou que a maioria das pessoas só deseja saber quanto o software custa, sem ser incomodada com teorias sobre propriedade intelectual.\n\nPortanto não o incomodarei e serei bem direto: você pode testar {name} de graça, mas deverá comprá-lo para usá-lo sem limitações. Em modo demo, {name} {limitation}.\n\nÉ simples assim. Caso você tenha curiosidade sobre fairware, recomendo que leia mais sobre o assunto clicando o botão \"Fairware?\"."

View File

@ -0,0 +1,20 @@
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: hscommon\n"
"PO-Revision-Date: 2013-04-28 18:29+0000\n"
"Last-Translator: hsoft <hsoft@hardcoded.net>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n"
"Language: ru\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
#: hscommon/reg.py:32
msgid ""
"{name} is fairware, which means \"open source software developed with expectation of fair contributions from users\". It's a very interesting concept, but one year of fairware has shown that most people just want to know how much it costs and not be bothered with theories about intellectual property.\n"
"\n"
"So I won't bother you and will be very straightforward: You can try {name} for free but you have to buy it in order to use it without limitations. In demo mode, {name} {limitation}.\n"
"\n"
"So it's as simple as this. If you're curious about fairware, however, I encourage you to read more about it by clicking on the \"Fairware?\" button."
msgstr "{name} является fairware, что означает \"программное обеспечение с открытым исходным кодом, с ожиданием справедливого вклада от пользователей\". Это очень интересная концепция, но один год fairware показал, что большинство людей просто хотят знать, сколько это стоит, а не возиться с теориями об интеллектуальной собственности.\n\nТак что, я не буду утомлять вас и буду очень краток: вы можете попробовать {name} бесплатно, но вам придётся купить её, чтобы использовать без ограничений. В демо-режиме {name} {limitation}.\n\nТо есть, это просто. Если вам интересно, о fairware вы можете больше узнать нажав на кнопку \"Fairware?\"."

View File

@ -0,0 +1,20 @@
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: hscommon\n"
"PO-Revision-Date: 2013-04-28 18:29+0000\n"
"Last-Translator: hsoft <hsoft@hardcoded.net>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n"
"Language: uk\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
#: hscommon/reg.py:32
msgid ""
"{name} is fairware, which means \"open source software developed with expectation of fair contributions from users\". It's a very interesting concept, but one year of fairware has shown that most people just want to know how much it costs and not be bothered with theories about intellectual property.\n"
"\n"
"So I won't bother you and will be very straightforward: You can try {name} for free but you have to buy it in order to use it without limitations. In demo mode, {name} {limitation}.\n"
"\n"
"So it's as simple as this. If you're curious about fairware, however, I encourage you to read more about it by clicking on the \"Fairware?\" button."
msgstr "{name} є fairware, що означає \"програмне забезпечення з відкритим вихідним кодом, з очікуванням справедливого вкладу від користувачів\". Це дуже цікава концепція, але один рік fairware показав, що більшість людей просто хочуть знати, скільки це коштує, а не возитися з теоріями про інтелектуальну власність.\n\nТож я не буду втомлювати Вас і поясню просто: Ви можете спробувати {name} безкоштовно, але Вам доведеться купити його, щоб використовувати її без обмежень. У демо-режимі, {name} {limitation}.\n\nОсь так це просто. Якщо Вас цікавить ідея fairware, то я запрошую Вас дізнатися більше про це, натиснувши на кнопку \"Fairware?\"."

View File

@ -0,0 +1,31 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: utf-8\n"
#: hscommon/reg.py:39
msgid ""
"{name} is Fairware, which means \"open source software developed with expectation of fair contributions from users\". Hours have been invested in this software with the expectation that users will be fair enough to compensate them. The \"Unpaid hours\" figure you see below is the hours that have yet to be compensated for this project.\n"
"\n"
"If you like this application, please make a contribution that you consider fair. Thanks!\n"
"\n"
"If you cannot afford to contribute, you can either ignore this reminder or send an e-mail at support@hardcoded.net so I can send you a registration key.\n"
"\n"
"This dialog doesn't show when there are no unpaid hours or when you have a valid contribution key."
msgstr ""
"{name} 是一款捐助软件,也就是说 \"用户对研发开源软件所花费的时间进行符合用户意愿的捐助\"。用户可以根据研发人员花费在开发软件上的时间进行合理的补偿。用户在下面看到的 \"未支付的时间\" (Unpaid hours)表示需要对该软件进行补偿的时间。\n"
" \n"
"如果您喜欢这款软件,我诚挚的希望您可以进行必要的捐助。谢谢!\n"
"\n"
"如果您无法承担捐助,您也可以忽略此提醒,或者发送电子邮件至 support@hardcoded.net ,我会发送给您一个注册密钥。\n"
"\n"
"当软件没有未支付的时间或您已使用一个有效的注册密钥,此对话框将不会再显示。"
#: hscommon/reg.py:51
msgid ""
"{name} is fairware, which means \"open source software developed with expectation of fair contributions from users\". It's a very interesting concept, but one year of fairware has shown that most people just want to know how much it costs and not be bothered with theories about intellectual property.\n"
"\n"
"So I won't bother you and will be very straightforward: You can try {name} for free but you have to buy it in order to use it without limitations. In demo mode, {name} {limitation}.\n"
"\n"
"So it's as simple as this. If you're curious about fairware, however, I encourage you to read more about it by clicking on the \"Fairware?\" button."
msgstr ""

69
hscommon/notify.py Normal file
View File

@ -0,0 +1,69 @@
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
from collections import defaultdict
class Broadcaster:
def __init__(self):
self.listeners = set()
def add_listener(self, listener):
self.listeners.add(listener)
def notify(self, msg):
for listener in self.listeners.copy(): # listeners can change during iteration
if listener in self.listeners: # disconnected during notification
listener.dispatch(msg)
def remove_listener(self, listener):
self.listeners.discard(listener)
class Listener:
def __init__(self, broadcaster):
self.broadcaster = broadcaster
self._bound_notifications = defaultdict(list)
def bind_messages(self, messages, func):
"""Binds multiple message to the same function.
Often, we perform the same thing on multiple messages. Instead of having the same function
repeated again and agin in our class, we can use this method to bind multiple messages to
the same function.
"""
for message in messages:
self._bound_notifications[message].append(func)
def connect(self):
self.broadcaster.add_listener(self)
def disconnect(self):
self.broadcaster.remove_listener(self)
def dispatch(self, msg):
if msg in self._bound_notifications:
for func in self._bound_notifications[msg]:
func()
if hasattr(self, msg):
method = getattr(self, msg)
method()
class Repeater(Broadcaster, Listener):
REPEATED_NOTIFICATIONS = None
def __init__(self, broadcaster):
Broadcaster.__init__(self)
Listener.__init__(self, broadcaster)
def _repeat_message(self, msg):
if not self.REPEATED_NOTIFICATIONS or msg in self.REPEATED_NOTIFICATIONS:
self.notify(msg)
def dispatch(self, msg):
Listener.dispatch(self, msg)
self._repeat_message(msg)

184
hscommon/path.py Executable file
View File

@ -0,0 +1,184 @@
# Created By: Virgil Dupras
# Created On: 2006/02/21
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
import logging
import os
import os.path as op
import shutil
import sys
from itertools import takewhile
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.
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)
"""
# Saves a little bit of memory usage
__slots__ = ()
def __new__(cls, value, separator=None):
def unicode_if_needed(s):
if isinstance(s, str):
return s
else:
try:
return str(s, sys.getfilesystemencoding())
except UnicodeDecodeError:
logging.warning("Could not decode %r", s)
raise
if isinstance(value, Path):
return value
if not separator:
separator = os.sep
if isinstance(value, bytes):
value = unicode_if_needed(value)
if isinstance(value, str):
if value:
if (separator not in value) and ('/' in value):
separator = '/'
value = value.split(separator)
else:
value = ()
else:
if any(isinstance(x, bytes) for x in value):
value = [unicode_if_needed(x) for x in value]
#value is a tuple/list
if any(separator in x for x in value):
#We have a component with a separator in it. Let's rejoin it, and generate another path.
return Path(separator.join(value), separator)
if (len(value) > 1) and (not value[-1]):
value = value[:-1] #We never want a path to end with a '' (because Path() can be called with a trailing slash ending path)
return tuple.__new__(cls, value)
def __add__(self, other):
other = Path(other)
if other and (not other[0]):
other = other[1:]
return Path(tuple.__add__(self, other))
def __contains__(self, item):
if isinstance(item, Path):
return item[:len(self)] == self
else:
return tuple.__contains__(self, item)
def __eq__(self, other):
return tuple.__eq__(self, Path(other))
def __getitem__(self, key):
if isinstance(key, slice):
if isinstance(key.start, Path):
equal_elems = list(takewhile(lambda pair: pair[0] == pair[1], zip(self, key.start)))
key = slice(len(equal_elems), key.stop, key.step)
if isinstance(key.stop, Path):
equal_elems = list(takewhile(lambda pair: pair[0] == pair[1], zip(reversed(self), reversed(key.stop))))
stop = -len(equal_elems) if equal_elems else None
key = slice(key.start, stop, key.step)
return Path(tuple.__getitem__(self, key))
else:
return tuple.__getitem__(self, key)
def __getslice__(self, i, j): #I have to override it because tuple uses it.
return Path(tuple.__getslice__(self, i, j))
def __hash__(self):
return tuple.__hash__(self)
def __ne__(self, other):
return not self.__eq__(other)
def __radd__(self, other):
return Path(other) + self
def __str__(self):
if len(self) == 1:
first = self[0]
if (len(first) == 2) and (first[1] == ':'): #Windows drive letter
return first + '\\'
elif not len(first): #root directory
return '/'
return os.sep.join(self)
def has_drive_letter(self):
if not self:
return False
first = self[0]
return (len(first) == 2) and (first[1] == ':')
def remove_drive_letter(self):
if self.has_drive_letter():
return self[1:]
else:
return self
def tobytes(self):
return str(self).encode(sys.getfilesystemencoding())
# OS method wrappers
def exists(self):
return op.exists(str(self))
def copy(self, dest_path):
return shutil.copy(str(self), str(dest_path))
def copytree(self, dest_path, *args, **kwargs):
return shutil.copytree(str(self), str(dest_path), *args, **kwargs)
def isdir(self):
return op.isdir(str(self))
def isfile(self):
return op.isfile(str(self))
def islink(self):
return op.islink(str(self))
def listdir(self):
return os.listdir(str(self))
def mkdir(self, *args, **kwargs):
return os.mkdir(str(self), *args, **kwargs)
def makedirs(self, *args, **kwargs):
return os.makedirs(str(self), *args, **kwargs)
def move(self, dest_path):
return shutil.move(str(self), str(dest_path))
def open(self, *args, **kwargs):
return open(str(self), *args, **kwargs)
def remove(self):
return os.remove(str(self))
def rename(self, dest_path):
return os.rename(str(self), str(dest_path))
def rmdir(self):
return os.rmdir(str(self))
def rmtree(self):
return shutil.rmtree(str(self))
def stat(self):
return os.stat(str(self))

16
hscommon/plat.py Normal file
View File

@ -0,0 +1,16 @@
# Created On: 2011/09/22
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
# Yes, I know, there's the 'platform' unit for this kind of stuff, but the thing is that I got a
# crash on startup once simply for importing this module and since then I don't trust it. One day,
# I'll investigate the cause of that crash further.
import sys
ISWINDOWS = sys.platform == 'win32'
ISOSX = sys.platform == 'darwin'
ISLINUX = sys.platform.startswith('linux')

417
hscommon/pygettext.py Executable file
View File

@ -0,0 +1,417 @@
# This module was taken from CPython's Tools/i18n and dirtily hacked to bypass the need for cmdline
# invocation.
# Originally written by Barry Warsaw <barry@zope.com>
#
# Minimally patched to make it even more xgettext compatible
# by Peter Funk <pf@artcom-gmbh.de>
#
# 2002-11-22 Jürgen Hermann <jh@web.de>
# Added checks that _() only contains string literals, and
# command line args are resolved to module lists, i.e. you
# can now pass a filename, a module or package name, or a
# directory (including globbing chars, important for Win32).
# Made docstring fit in 80 chars wide displays using pydoc.
#
import os
import imp
import sys
import glob
import time
import token
import tokenize
import operator
__version__ = '1.5'
default_keywords = ['_']
DEFAULTKEYWORDS = ', '.join(default_keywords)
EMPTYSTRING = ''
# The normal pot-file header. msgmerge and Emacs's po-mode work better if it's
# there.
pot_header = """
msgid ""
msgstr ""
"Content-Type: text/plain; charset=utf-8\\n"
"Content-Transfer-Encoding: utf-8\\n"
"""
def usage(code, msg=''):
print(__doc__ % globals(), file=sys.stderr)
if msg:
print(msg, file=sys.stderr)
sys.exit(code)
escapes = []
def make_escapes(pass_iso8859):
global escapes
if pass_iso8859:
# Allow iso-8859 characters to pass through so that e.g. 'msgid
# "H?he"' would result not result in 'msgid "H\366he"'. Otherwise we
# escape any character outside the 32..126 range.
mod = 128
else:
mod = 256
for i in range(256):
if 32 <= (i % mod) <= 126:
escapes.append(chr(i))
else:
escapes.append("\\%03o" % i)
escapes[ord('\\')] = '\\\\'
escapes[ord('\t')] = '\\t'
escapes[ord('\r')] = '\\r'
escapes[ord('\n')] = '\\n'
escapes[ord('\"')] = '\\"'
def escape(s):
global escapes
s = list(s)
for i in range(len(s)):
s[i] = escapes[ord(s[i])]
return EMPTYSTRING.join(s)
def safe_eval(s):
# unwrap quotes, safely
return eval(s, {'__builtins__':{}}, {})
def normalize(s):
# This converts the various Python string types into a format that is
# appropriate for .po files, namely much closer to C style.
lines = s.split('\n')
if len(lines) == 1:
s = '"' + escape(s) + '"'
else:
if not lines[-1]:
del lines[-1]
lines[-1] = lines[-1] + '\n'
for i in range(len(lines)):
lines[i] = escape(lines[i])
lineterm = '\\n"\n"'
s = '""\n"' + lineterm.join(lines) + '"'
return s
def containsAny(str, set):
"""Check whether 'str' contains ANY of the chars in 'set'"""
return 1 in [c in str for c in set]
def _visit_pyfiles(list, dirname, names):
"""Helper for getFilesForName()."""
# get extension for python source files
if '_py_ext' not in globals():
global _py_ext
_py_ext = [triple[0] for triple in imp.get_suffixes()
if triple[2] == imp.PY_SOURCE][0]
# don't recurse into CVS directories
if 'CVS' in names:
names.remove('CVS')
# add all *.py files to list
list.extend(
[os.path.join(dirname, file) for file in names
if os.path.splitext(file)[1] == _py_ext]
)
def _get_modpkg_path(dotted_name, pathlist=None):
"""Get the filesystem path for a module or a package.
Return the file system path to a file for a module, and to a directory for
a package. Return None if the name is not found, or is a builtin or
extension module.
"""
# split off top-most name
parts = dotted_name.split('.', 1)
if len(parts) > 1:
# we have a dotted path, import top-level package
try:
file, pathname, description = imp.find_module(parts[0], pathlist)
if file: file.close()
except ImportError:
return None
# check if it's indeed a package
if description[2] == imp.PKG_DIRECTORY:
# recursively handle the remaining name parts
pathname = _get_modpkg_path(parts[1], [pathname])
else:
pathname = None
else:
# plain name
try:
file, pathname, description = imp.find_module(
dotted_name, pathlist)
if file:
file.close()
if description[2] not in [imp.PY_SOURCE, imp.PKG_DIRECTORY]:
pathname = None
except ImportError:
pathname = None
return pathname
def getFilesForName(name):
"""Get a list of module files for a filename, a module or package name,
or a directory.
"""
if not os.path.exists(name):
# check for glob chars
if containsAny(name, "*?[]"):
files = glob.glob(name)
list = []
for file in files:
list.extend(getFilesForName(file))
return list
# try to find module or package
name = _get_modpkg_path(name)
if not name:
return []
if os.path.isdir(name):
# find all python files in directory
list = []
os.walk(name, _visit_pyfiles, list)
return list
elif os.path.exists(name):
# a single file
return [name]
return []
class TokenEater:
def __init__(self, options):
self.__options = options
self.__messages = {}
self.__state = self.__waiting
self.__data = []
self.__lineno = -1
self.__freshmodule = 1
self.__curfile = None
def __call__(self, ttype, tstring, stup, etup, line):
# dispatch
## import token
## print >> sys.stderr, 'ttype:', token.tok_name[ttype], \
## 'tstring:', tstring
self.__state(ttype, tstring, stup[0])
def __waiting(self, ttype, tstring, lineno):
opts = self.__options
# Do docstring extractions, if enabled
if opts.docstrings and not opts.nodocstrings.get(self.__curfile):
# module docstring?
if self.__freshmodule:
if ttype == tokenize.STRING:
self.__addentry(safe_eval(tstring), lineno, isdocstring=1)
self.__freshmodule = 0
elif ttype not in (tokenize.COMMENT, tokenize.NL):
self.__freshmodule = 0
return
# class docstring?
if ttype == tokenize.NAME and tstring in ('class', 'def'):
self.__state = self.__suiteseen
return
if ttype == tokenize.NAME and tstring in opts.keywords:
self.__state = self.__keywordseen
def __suiteseen(self, ttype, tstring, lineno):
# ignore anything until we see the colon
if ttype == tokenize.OP and tstring == ':':
self.__state = self.__suitedocstring
def __suitedocstring(self, ttype, tstring, lineno):
# ignore any intervening noise
if ttype == tokenize.STRING:
self.__addentry(safe_eval(tstring), lineno, isdocstring=1)
self.__state = self.__waiting
elif ttype not in (tokenize.NEWLINE, tokenize.INDENT,
tokenize.COMMENT):
# there was no class docstring
self.__state = self.__waiting
def __keywordseen(self, ttype, tstring, lineno):
if ttype == tokenize.OP and tstring == '(':
self.__data = []
self.__lineno = lineno
self.__state = self.__openseen
else:
self.__state = self.__waiting
def __openseen(self, ttype, tstring, lineno):
if ttype == tokenize.OP and tstring == ')':
# We've seen the last of the translatable strings. Record the
# line number of the first line of the strings and update the list
# of messages seen. Reset state for the next batch. If there
# were no strings inside _(), then just ignore this entry.
if self.__data:
self.__addentry(EMPTYSTRING.join(self.__data))
self.__state = self.__waiting
elif ttype == tokenize.STRING:
self.__data.append(safe_eval(tstring))
elif ttype not in [tokenize.COMMENT, token.INDENT, token.DEDENT,
token.NEWLINE, tokenize.NL]:
# warn if we see anything else than STRING or whitespace
print('*** %(file)s:%(lineno)s: Seen unexpected token "%(token)s"' % {
'token': tstring,
'file': self.__curfile,
'lineno': self.__lineno
}, file=sys.stderr)
self.__state = self.__waiting
def __addentry(self, msg, lineno=None, isdocstring=0):
if lineno is None:
lineno = self.__lineno
if not msg in self.__options.toexclude:
entry = (self.__curfile, lineno)
self.__messages.setdefault(msg, {})[entry] = isdocstring
def set_filename(self, filename):
self.__curfile = filename
self.__freshmodule = 1
def write(self, fp):
options = self.__options
timestamp = time.strftime('%Y-%m-%d %H:%M+%Z')
# The time stamp in the header doesn't have the same format as that
# generated by xgettext...
print(pot_header, file=fp)
# Sort the entries. First sort each particular entry's keys, then
# sort all the entries by their first item.
reverse = {}
for k, v in self.__messages.items():
keys = sorted(v.keys())
reverse.setdefault(tuple(keys), []).append((k, v))
rkeys = sorted(reverse.keys())
for rkey in rkeys:
rentries = reverse[rkey]
rentries.sort()
for k, v in rentries:
# If the entry was gleaned out of a docstring, then add a
# comment stating so. This is to aid translators who may wish
# to skip translating some unimportant docstrings.
isdocstring = any(v.values())
# k is the message string, v is a dictionary-set of (filename,
# lineno) tuples. We want to sort the entries in v first by
# file name and then by line number.
v = sorted(v.keys())
if not options.writelocations:
pass
# location comments are different b/w Solaris and GNU:
elif options.locationstyle == options.SOLARIS:
for filename, lineno in v:
d = {'filename': filename, 'lineno': lineno}
print('# File: %(filename)s, line: %(lineno)d' % d, file=fp)
elif options.locationstyle == options.GNU:
# fit as many locations on one line, as long as the
# resulting line length doesn't exceeds 'options.width'
locline = '#:'
for filename, lineno in v:
d = {'filename': filename, 'lineno': lineno}
s = ' %(filename)s:%(lineno)d' % d
if len(locline) + len(s) <= options.width:
locline = locline + s
else:
print(locline, file=fp)
locline = "#:" + s
if len(locline) > 2:
print(locline, file=fp)
if isdocstring:
print('#, docstring', file=fp)
print('msgid', normalize(k), file=fp)
print('msgstr ""\n', file=fp)
def main(source_files, outpath, keywords=None):
global default_keywords
# for holding option values
class Options:
# constants
GNU = 1
SOLARIS = 2
# defaults
extractall = 0 # FIXME: currently this option has no effect at all.
escape = 0
keywords = []
outfile = 'messages.pot'
writelocations = 1
locationstyle = GNU
verbose = 0
width = 78
excludefilename = ''
docstrings = 0
nodocstrings = {}
options = Options()
locations = {'gnu' : options.GNU,
'solaris' : options.SOLARIS,
}
options.outfile = outpath
if keywords:
options.keywords = keywords
# calculate escapes
make_escapes(options.escape)
# calculate all keywords
options.keywords.extend(default_keywords)
# initialize list of strings to exclude
if options.excludefilename:
try:
fp = open(options.excludefilename)
options.toexclude = fp.readlines()
fp.close()
except IOError:
print("Can't read --exclude-file: %s" % options.excludefilename, file=sys.stderr)
sys.exit(1)
else:
options.toexclude = []
# slurp through all the files
eater = TokenEater(options)
for filename in source_files:
if options.verbose:
print('Working on %s' % filename)
fp = open(filename)
closep = 1
try:
eater.set_filename(filename)
try:
tokens = tokenize.generate_tokens(fp.readline)
for _token in tokens:
eater(*_token)
except tokenize.TokenError as e:
print('%s: %s, line %d, column %d' % (
e.args[0], filename, e.args[1][0], e.args[1][1]),
file=sys.stderr)
finally:
if closep:
fp.close()
fp = open(options.outfile, 'w')
closep = 1
try:
eater.write(fp)
finally:
if closep:
fp.close()

179
hscommon/reg.py Normal file
View File

@ -0,0 +1,179 @@
# Created By: Virgil Dupras
# Created On: 2009-05-16
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
import re
from hashlib import md5
from .trans import trget
tr = trget('hscommon')
ALL_APPS = [
(1, 'dupeGuru'),
(2, 'moneyGuru'),
(3, 'musicGuru'),
(6, 'PdfMasher'),
]
OLDAPPIDS = {
1: {1, 4, 5},
2: {6, },
3: {2, },
}
class InvalidCodeError(Exception):
"""The supplied code is invalid."""
DEMO_PROMPT = tr("{name} is fairware, which means \"open source software developed with expectation "
"of fair contributions from users\". It's a very interesting concept, but one year of fairware has "
"shown that most people just want to know how much it costs and not be bothered with theories "
"about intellectual property."
"\n\n"
"So I won't bother you and will be very straightforward: You can try {name} for free but you have "
"to buy it in order to use it without limitations. In demo mode, {name} {limitation}."
"\n\n"
"So it's as simple as this. If you're curious about fairware, however, I encourage you to read "
"more about it by clicking on the \"Fairware?\" button.")
class RegistrableApplication:
#--- View interface
# get_default(key_name)
# set_default(key_name, value)
# setup_as_registered()
# show_message(msg)
# show_demo_nag(prompt)
# open_url(url)
PROMPT_NAME = "<undefined>"
DEMO_LIMITATION = "<undefined>"
def __init__(self, view, appid):
self.view = view
self.appid = appid
self.registered = False
self.fairware_mode = False
self.registration_code = ''
self.registration_email = ''
self._unpaid_hours = None
@staticmethod
def _is_code_valid(appid, code, email):
if len(code) != 32:
return False
appid = str(appid)
for i in range(100):
blob = appid + email + str(i) + 'aybabtu'
digest = md5(blob.encode('utf-8')).hexdigest()
if digest == code:
return True
return False
def _set_registration(self, code, email):
self.validate_code(code, email)
self.registration_code = code
self.registration_email = email
self.registered = True
self.view.setup_as_registered()
def initial_registration_setup(self):
# Should be called only after the app is finished launching
if self.registered:
# We've already set registration in a hardcoded way (for example, for the Ubuntu Store)
# Just ignore registration, but not before having set as registered.
self.view.setup_as_registered()
return
code = self.view.get_default('RegistrationCode')
email = self.view.get_default('RegistrationEmail')
if code and email:
try:
self._set_registration(code, email)
except InvalidCodeError:
pass
if not self.registered:
if self.view.get_default('FairwareMode'):
self.fairware_mode = True
if not self.fairware_mode:
prompt = DEMO_PROMPT.format(name=self.PROMPT_NAME, limitation=self.DEMO_LIMITATION)
self.view.show_demo_nag(prompt)
def validate_code(self, code, email):
code = code.strip().lower()
email = email.strip().lower()
if self._is_code_valid(self.appid, code, email):
return
# Check if it's not an old reg code
for oldappid in OLDAPPIDS.get(self.appid, []):
if self._is_code_valid(oldappid, code, email):
return
# let's see if the user didn't mix the fields up
if self._is_code_valid(self.appid, email, code):
msg = "Invalid Code. It seems like you inverted the 'Registration Code' and"\
"'Registration E-mail' field."
raise InvalidCodeError(msg)
# Is the code a paypal transaction id?
if re.match(r'^[a-z\d]{17}$', code) is not None:
msg = "The code you submitted looks like a Paypal transaction ID. Registration codes are "\
"32 digits codes which you should have received in a separate e-mail. If you haven't "\
"received it yet, please visit http://www.hardcoded.net/support/"
raise InvalidCodeError(msg)
# Invalid, let's see if it's a code for another app.
for appid, appname in ALL_APPS:
if self._is_code_valid(appid, code, email):
msg = "This code is a {0} code. You're running the wrong application. You can "\
"download the correct application at http://www.hardcoded.net".format(appname)
raise InvalidCodeError(msg)
DEFAULT_MSG = "Your code is invalid. Make sure that you wrote the good code. Also make sure "\
"that the e-mail you gave is the same as the e-mail you used for your purchase."
raise InvalidCodeError(DEFAULT_MSG)
def set_registration(self, code, email, register_os):
if not self.fairware_mode and 'fairware' in {code.strip().lower(), email.strip().lower()}:
self.fairware_mode = True
self.view.set_default('FairwareMode', True)
self.view.show_message("Fairware mode enabled.")
return True
try:
self._set_registration(code, email)
self.view.show_message("Your code is valid. Thanks!")
if register_os:
self.register_os()
self.view.set_default('RegistrationCode', self.registration_code)
self.view.set_default('RegistrationEmail', self.registration_email)
return True
except InvalidCodeError as e:
self.view.show_message(str(e))
return False
def register_os(self):
# We don't do that anymore.
pass
def contribute(self):
self.view.open_url("http://open.hardcoded.net/contribute/")
def buy(self):
self.view.open_url("http://www.hardcoded.net/purchase.htm")
def about_fairware(self):
self.view.open_url("http://open.hardcoded.net/about/")
@property
def should_show_fairware_reminder(self):
return (not self.registered) and (self.fairware_mode) and (self.unpaid_hours >= 1)
@property
def should_apply_demo_limitation(self):
return (not self.registered) and (not self.fairware_mode)
@property
def unpaid_hours(self):
# We don't bother verifying unpaid hours anymore, the only app that still has fairware
# dialogs is dupeGuru and it has a huge surplus. Now, "fairware mode" really means
# "free mode".
return 0

62
hscommon/sphinxgen.py Normal file
View File

@ -0,0 +1,62 @@
# Created By: Virgil Dupras
# Created On: 2011-01-12
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
import os.path as op
import re
from .build import print_and_do, read_changelog_file, filereplace
CHANGELOG_FORMAT = """
{version} ({date})
----------------------
{description}
"""
def tixgen(tixurl):
"""This is a filter *generator*. tixurl is a url pattern for the tix with a {0} placeholder
for the tix #
"""
urlpattern = tixurl.format('\\1') # will be replaced buy the content of the first group in re
R = re.compile(r'#(\d+)')
repl = '`#\\1 <{}>`__'.format(urlpattern)
return lambda text: R.sub(repl, text)
def gen(basepath, destpath, changelogpath, tixurl, confrepl=None, confpath=None, changelogtmpl=None):
"""Generate sphinx docs with all bells and whistles.
basepath: The base sphinx source path.
destpath: The final path of html files
changelogpath: The path to the changelog file to insert in changelog.rst.
tixurl: The URL (with one formattable argument for the tix number) to the ticket system.
confrepl: Dictionary containing replacements that have to be made in conf.py. {name: replacement}
"""
if confrepl is None:
confrepl = {}
if confpath is None:
confpath = op.join(basepath, 'conf.tmpl')
if changelogtmpl is None:
changelogtmpl = op.join(basepath, 'changelog.tmpl')
changelog = read_changelog_file(changelogpath)
tix = tixgen(tixurl)
rendered_logs = []
for log in changelog:
description = tix(log['description'])
# The format of the changelog descriptions is in markdown, but since we only use bulled list
# and links, it's not worth depending on the markdown package. A simple regexp suffice.
description = re.sub(r'\[(.*?)\]\((.*?)\)', '`\\1 <\\2>`__', description)
rendered = CHANGELOG_FORMAT.format(version=log['version'], date=log['date_str'],
description=description)
rendered_logs.append(rendered)
confrepl['version'] = changelog[0]['version']
changelog_out = op.join(basepath, 'changelog.rst')
filereplace(changelogtmpl, changelog_out, changelog='\n'.join(rendered_logs))
conf_out = op.join(basepath, 'conf.py')
filereplace(confpath, conf_out, **confrepl)
cmd = 'sphinx-build "{}" "{}"'.format(basepath, destpath)
print_and_do(cmd)

137
hscommon/sqlite.py Normal file
View File

@ -0,0 +1,137 @@
# Created By: Virgil Dupras
# Created On: 2007/05/19
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
import sys
import os
import os.path as op
import threading
from queue import Queue
import time
import sqlite3 as sqlite
STOP = object()
COMMIT = object()
ROLLBACK = object()
class FakeCursor(list):
# It's not possible to use sqlite cursors on another thread than the connection. Thus,
# we can't directly return the cursor. We have to fatch all results, and support its interface.
def fetchall(self):
return self
def fetchone(self):
try:
return self.pop(0)
except IndexError:
return None
class _ActualThread(threading.Thread):
''' We can't use this class directly because thread object are not automatically freed when
nothing refers to it, making it hang the application if not explicitely closed.
'''
def __init__(self, dbname, autocommit):
threading.Thread.__init__(self)
self._queries = Queue()
self._results = Queue()
self._dbname = dbname
self._autocommit = autocommit
self._waiting_list = set()
self._lock = threading.Lock()
self._run = True
self.lastrowid = -1
self.setDaemon(True)
self.start()
def _query(self, query):
with self._lock:
wait_token = object()
self._waiting_list.add(wait_token)
self._queries.put(query)
self._waiting_list.remove(wait_token)
result = self._results.get()
return result
def close(self):
if not self._run:
return
self._query(STOP)
def commit(self):
if not self._run:
return None # Connection closed
self._query(COMMIT)
def execute(self, sql, values=()):
if not self._run:
return None # Connection closed
result = self._query((sql, values))
if isinstance(result, Exception):
raise result
return result
def rollback(self):
if not self._run:
return None # Connection closed
self._query(ROLLBACK)
def run(self):
# The whole chdir thing is because sqlite doesn't handle directory names with non-asci char in the AT ALL.
oldpath = os.getcwd()
dbdir, dbname = op.split(self._dbname)
if dbdir:
os.chdir(dbdir)
if self._autocommit:
con = sqlite.connect(dbname, isolation_level=None)
else:
con = sqlite.connect(dbname)
os.chdir(oldpath)
while self._run or self._waiting_list:
query = self._queries.get()
result = None
if query is STOP:
self._run = False
elif query is COMMIT:
con.commit()
elif query is ROLLBACK:
con.rollback()
else:
sql, values = query
try:
cur = con.execute(sql, values)
self.lastrowid = cur.lastrowid
result = FakeCursor(cur.fetchall())
result.lastrowid = cur.lastrowid
except Exception as e:
result = e
self._results.put(result)
con.close()
class ThreadedConn:
def __init__(self, dbname, autocommit):
self._t = _ActualThread(dbname, autocommit)
self.lastrowid = -1
def __del__(self):
self.close()
def close(self):
self._t.close()
def commit(self):
self._t.commit()
def execute(self, sql, values=()):
result = self._t.execute(sql, values)
self.lastrowid = self._t.lastrowid
return result
def rollback(self):
self._t.rollback()

View File

View File

@ -0,0 +1,104 @@
# Created By: Virgil Dupras
# Created On: 2008-01-08
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
from ..conflict import *
from ..path import Path
from ..testutil import eq_
class TestCase_GetConflictedName:
def test_simple(self):
name = get_conflicted_name(['bar'], 'bar')
eq_('[000] bar', name)
name = get_conflicted_name(['bar', '[000] bar'], 'bar')
eq_('[001] bar', name)
def test_no_conflict(self):
name = get_conflicted_name(['bar'], 'foobar')
eq_('foobar', name)
def test_fourth_digit(self):
# This test is long because every time we have to add a conflicted name,
# a test must be made for every other conflicted name existing...
# Anyway, this has very few chances to happen.
names = ['bar'] + ['[%03d] bar' % i for i in range(1000)]
name = get_conflicted_name(names, 'bar')
eq_('[1000] bar', name)
def test_auto_unconflict(self):
# Automatically unconflict the name if it's already conflicted.
name = get_conflicted_name([], '[000] foobar')
eq_('foobar', name)
name = get_conflicted_name(['bar'], '[001] bar')
eq_('[000] bar', name)
class TestCase_GetUnconflictedName:
def test_main(self):
eq_('foobar',get_unconflicted_name('[000] foobar'))
eq_('foobar',get_unconflicted_name('[9999] foobar'))
eq_('[000]foobar',get_unconflicted_name('[000]foobar'))
eq_('[000a] foobar',get_unconflicted_name('[000a] foobar'))
eq_('foobar',get_unconflicted_name('foobar'))
eq_('foo [000] bar',get_unconflicted_name('foo [000] bar'))
class TestCase_IsConflicted:
def test_main(self):
assert is_conflicted('[000] foobar')
assert is_conflicted('[9999] foobar')
assert not is_conflicted('[000]foobar')
assert not is_conflicted('[000a] foobar')
assert not is_conflicted('foobar')
assert not is_conflicted('foo [000] bar')
class TestCase_move_copy:
def pytest_funcarg__do_setup(self, request):
tmpdir = request.getfuncargvalue('tmpdir')
self.path = Path(str(tmpdir))
io.open(self.path + 'foo', 'w').close()
io.open(self.path + 'bar', 'w').close()
io.mkdir(self.path + 'dir')
def test_move_no_conflict(self, do_setup):
smart_move(self.path + 'foo', self.path + 'baz')
assert io.exists(self.path + 'baz')
assert not io.exists(self.path + 'foo')
def test_copy_no_conflict(self, do_setup): # No need to duplicate the rest of the tests... Let's just test on move
smart_copy(self.path + 'foo', self.path + 'baz')
assert io.exists(self.path + 'baz')
assert io.exists(self.path + 'foo')
def test_move_no_conflict_dest_is_dir(self, do_setup):
smart_move(self.path + 'foo', self.path + 'dir')
assert io.exists(self.path + ('dir', 'foo'))
assert not io.exists(self.path + 'foo')
def test_move_conflict(self, do_setup):
smart_move(self.path + 'foo', self.path + 'bar')
assert io.exists(self.path + '[000] bar')
assert not io.exists(self.path + 'foo')
def test_move_conflict_dest_is_dir(self, do_setup):
smart_move(self.path + 'foo', self.path + 'dir')
smart_move(self.path + 'bar', self.path + 'foo')
smart_move(self.path + 'foo', self.path + 'dir')
assert io.exists(self.path + ('dir', 'foo'))
assert io.exists(self.path + ('dir', '[000] foo'))
assert not io.exists(self.path + 'foo')
assert not io.exists(self.path + 'bar')
def test_copy_folder(self, tmpdir):
# smart_copy also works on folders
path = Path(str(tmpdir))
io.mkdir(path + 'foo')
io.mkdir(path + 'bar')
smart_copy(path + 'foo', path + 'bar') # no crash
assert io.exists(path + '[000] bar')

View File

@ -0,0 +1,209 @@
# Created By: Virgil Dupras
# Created On: 2008-04-20
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
from datetime import date
import sqlite3 as sqlite
from .. import io
from ..testutil import eq_, assert_almost_equal
from ..currency import Currency, RatesDB, CAD, EUR, PLN, USD
def setup_module(module):
global FOO
global BAR
FOO = Currency.register('FOO', 'Currency with start date', start_date=date(2009, 1, 12), start_rate=2)
BAR = Currency.register('BAR', 'Currency with stop date', stop_date=date(2010, 1, 12), latest_rate=2)
def teardown_module(module):
# We must unset our test currencies or else we might mess up with other tests.
from .. import currency
import imp
imp.reload(currency)
def teardown_function(function):
Currency.set_rates_db(None)
def test_currency_creation():
# Different ways to create a currency.
eq_(Currency('CAD'), CAD)
eq_(Currency(name='Canadian dollar'), CAD)
def test_currency_copy():
# Currencies can be copied.
import copy
eq_(copy.copy(CAD), CAD)
eq_(copy.deepcopy(CAD), CAD)
def test_get_rate_on_empty_db():
# When there is no data available, use the start_rate.
eq_(CAD.value_in(USD, date(2008, 4, 20)), 1 / USD.latest_rate)
def test_physical_rates_db_remember_rates(tmpdir):
# When a rates db uses a real file, rates are remembered
dbpath = str(tmpdir.join('foo.db'))
db = RatesDB(dbpath)
db.set_CAD_value(date(2008, 4, 20), 'USD', 1/0.996115)
db = RatesDB(dbpath)
assert_almost_equal(db.get_rate(date(2008, 4, 20), 'CAD', 'USD'), 0.996115)
def test_db_with_connection():
# the supplied connection is used by the rates db.
con = sqlite.connect(':memory:')
db = RatesDB(con)
try:
con.execute("select * from rates where 1=2")
except sqlite.OperationalError: # new db
raise AssertionError()
def test_corrupt_db(tmpdir):
dbpath = str(tmpdir.join('foo.db'))
fh = io.open(dbpath, 'w')
fh.write('corrupted')
fh.close()
db = RatesDB(dbpath) # no crash. deletes the old file and start a new db
db.set_CAD_value(date(2008, 4, 20), 'USD', 42)
db = RatesDB(dbpath)
eq_(db.get_rate(date(2008, 4, 20), 'USD', 'CAD'), 42)
#--- Daily rate
def setup_daily_rate():
USD.set_CAD_value(1/0.996115, date(2008, 4, 20))
def test_get_rate_with_daily_rate():
# Getting the rate exactly as set_rate happened returns the same rate.
setup_daily_rate()
assert_almost_equal(CAD.value_in(USD, date(2008, 4, 20)), 0.996115)
def test_get_rate_different_currency():
# Use fallback rates when necessary.
setup_daily_rate()
eq_(CAD.value_in(EUR, date(2008, 4, 20)), 1 / EUR.latest_rate)
eq_(EUR.value_in(USD, date(2008, 4, 20)), EUR.latest_rate * 0.996115)
def test_get_rate_reverse():
# It's possible to get the reverse value of a rate using the same data.
setup_daily_rate()
assert_almost_equal(USD.value_in(CAD, date(2008, 4, 20)), 1 / 0.996115)
def test_set_rate_twice():
# When setting a rate for an index that already exists, the old rate is replaced by the new.
setup_daily_rate()
USD.set_CAD_value(1/42, date(2008, 4, 20))
assert_almost_equal(CAD.value_in(USD, date(2008, 4, 20)), 42)
def test_set_rate_after_get():
# When setting a rate after a get of the same rate, the rate cache is correctly updated.
setup_daily_rate()
CAD.value_in(USD, date(2008, 4, 20)) # value will be cached
USD.set_CAD_value(1/42, date(2008, 4, 20))
assert_almost_equal(CAD.value_in(USD, date(2008, 4, 20)), 42)
def test_set_rate_after_get_the_day_after():
# When setting a rate, the cache for the whole currency is reset, or else we get old fallback
# values for dates where the currency server returned no value.
setup_daily_rate()
CAD.value_in(USD, date(2008, 4, 21)) # value will be cached
USD.set_CAD_value(1/42, date(2008, 4, 20))
assert_almost_equal(CAD.value_in(USD, date(2008, 4, 21)), 42)
#--- Two daily rates
def setup_two_daily_rate():
# Don't change the set order, it's important for the tests
USD.set_CAD_value(1/0.997115, date(2008, 4, 25))
USD.set_CAD_value(1/0.996115, date(2008, 4, 20))
def test_date_range_range():
# USD.rates_date_range() returns the USD's limits.
setup_two_daily_rate()
eq_(USD.rates_date_range(), (date(2008, 4, 20), date(2008, 4, 25)))
def test_date_range_for_unfetched_currency():
# If the curency is not in the DB, return None.
setup_two_daily_rate()
assert PLN.rates_date_range() is None
def test_seek_rate_middle():
# A rate request with seek in the middle will return the lowest date.
setup_two_daily_rate()
eq_(USD.value_in(CAD, date(2008, 4, 24)), 1/0.996115)
def test_seek_rate_after():
# Make sure that the *nearest* lowest rate is returned. Because the 25th have been set
# before the 20th, an order by clause is required in the seek SQL to make this test pass.
setup_two_daily_rate()
eq_(USD.value_in(CAD, date(2008, 4, 26)), 1/0.997115)
def test_seek_rate_before():
# If there are no rate in the past, seek for a rate in the future.
setup_two_daily_rate()
eq_(USD.value_in(CAD, date(2008, 4, 19)), 1/0.996115)
#--- Rates of multiple currencies
def setup_rates_of_multiple_currencies():
USD.set_CAD_value(1/0.996115, date(2008, 4, 20))
EUR.set_CAD_value(1/0.633141, date(2008, 4, 20))
def test_get_rate_multiple_currencies():
# Don't mix currency rates up.
setup_rates_of_multiple_currencies()
assert_almost_equal(CAD.value_in(USD, date(2008, 4, 20)), 0.996115)
assert_almost_equal(CAD.value_in(EUR, date(2008, 4, 20)), 0.633141)
def test_get_rate_with_pivotal():
# It's possible to get a rate by using 2 records.
# if 1 CAD = 0.996115 USD and 1 CAD = 0.633141 then 0.996115 USD = 0.633141 then 1 USD = 0.633141 / 0.996115 EUR
setup_rates_of_multiple_currencies()
assert_almost_equal(USD.value_in(EUR, date(2008, 4, 20)), 0.633141 / 0.996115)
def test_get_rate_doesnt_exist():
# Don't crash when trying to do pivotal calculation with non-existing currencies.
setup_rates_of_multiple_currencies()
eq_(USD.value_in(PLN, date(2008, 4, 20)), 1 / 0.996115 / PLN.latest_rate)
#--- Problems after connection
def get_problematic_db():
class MockConnection(sqlite.Connection): # can't mock sqlite3.Connection's attribute, so we subclass it
mocking = False
def execute(self, *args, **kwargs):
if self.mocking:
raise sqlite.OperationalError()
else:
return sqlite.Connection.execute(self, *args, **kwargs)
con = MockConnection(':memory:')
db = RatesDB(con)
con.mocking = True
return db
def test_date_range_with_problematic_db():
db = get_problematic_db()
db.date_range('USD') # no crash
def test_get_rate_with_problematic_db():
db = get_problematic_db()
db.get_rate(date(2008, 4, 20), 'USD', 'CAD') # no crash
def test_set_rate_with_problematic_db():
db = get_problematic_db()
db.set_CAD_value(date(2008, 4, 20), 'USD', 42) # no crash
#--- DB that doesn't allow get_rate calls
def setup_db_raising_error_on_getrate():
db = RatesDB()
def mock_get_rate(*args, **kwargs):
raise AssertionError()
db.get_rate = mock_get_rate
Currency.set_rates_db(db)
def test_currency_with_start_date():
setup_db_raising_error_on_getrate()
eq_(FOO.value_in(CAD, date(2009, 1, 11)), 2)
def test_currency_with_stop_date():
setup_db_raising_error_on_getrate()
eq_(BAR.value_in(CAD, date(2010, 1, 13)), 2)

View File

@ -0,0 +1,140 @@
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
from ..testutil import eq_
from ..notify import Broadcaster, Listener, Repeater
class HelloListener(Listener):
def __init__(self, broadcaster):
Listener.__init__(self, broadcaster)
self.hello_count = 0
def hello(self):
self.hello_count += 1
class HelloRepeater(Repeater):
def __init__(self, broadcaster):
Repeater.__init__(self, broadcaster)
self.hello_count = 0
def hello(self):
self.hello_count += 1
def create_pair():
b = Broadcaster()
l = HelloListener(b)
return b, l
def test_disconnect_during_notification():
# When a listener disconnects another listener the other listener will not receive a
# notification.
# This whole complication scheme below is because the order of the notification is not
# guaranteed. We could disconnect everything from self.broadcaster.listeners, but this
# member is supposed to be private. Hence, the '.other' scheme
class Disconnecter(Listener):
def __init__(self, broadcaster):
Listener.__init__(self, broadcaster)
self.hello_count = 0
def hello(self):
self.hello_count += 1
self.other.disconnect()
broadcaster = Broadcaster()
first = Disconnecter(broadcaster)
second = Disconnecter(broadcaster)
first.other, second.other = second, first
first.connect()
second.connect()
broadcaster.notify('hello')
# only one of them was notified
eq_(first.hello_count + second.hello_count, 1)
def test_disconnect():
# After a disconnect, the listener doesn't hear anything.
b, l = create_pair()
l.connect()
l.disconnect()
b.notify('hello')
eq_(l.hello_count, 0)
def test_disconnect_when_not_connected():
# When disconnecting an already disconnected listener, nothing happens.
b, l = create_pair()
l.disconnect()
def test_not_connected_on_init():
# A listener is not initialized connected.
b, l = create_pair()
b.notify('hello')
eq_(l.hello_count, 0)
def test_notify():
# The listener listens to the broadcaster.
b, l = create_pair()
l.connect()
b.notify('hello')
eq_(l.hello_count, 1)
def test_reconnect():
# It's possible to reconnect a listener after disconnection.
b, l = create_pair()
l.connect()
l.disconnect()
l.connect()
b.notify('hello')
eq_(l.hello_count, 1)
def test_repeater():
b = Broadcaster()
r = HelloRepeater(b)
l = HelloListener(r)
r.connect()
l.connect()
b.notify('hello')
eq_(r.hello_count, 1)
eq_(l.hello_count, 1)
def test_repeater_with_repeated_notifications():
# If REPEATED_NOTIFICATIONS is not empty, only notifs in this set are repeated (but they're
# still dispatched locally).
class MyRepeater(HelloRepeater):
REPEATED_NOTIFICATIONS = set(['hello'])
def __init__(self, broadcaster):
HelloRepeater.__init__(self, broadcaster)
self.foo_count = 0
def foo(self):
self.foo_count += 1
b = Broadcaster()
r = MyRepeater(b)
l = HelloListener(r)
r.connect()
l.connect()
b.notify('hello')
b.notify('foo') # if the repeater repeated this notif, we'd get a crash on HelloListener
eq_(r.hello_count, 1)
eq_(l.hello_count, 1)
eq_(r.foo_count, 1)
def test_repeater_doesnt_try_to_dispatch_to_self_if_it_cant():
# if a repeater doesn't handle a particular message, it doesn't crash and simply repeats it.
b = Broadcaster()
r = Repeater(b) # doesnt handle hello
l = HelloListener(r)
r.connect()
l.connect()
b.notify('hello') # no crash
eq_(l.hello_count, 1)
def test_bind_messages():
b, l = create_pair()
l.bind_messages({'foo', 'bar'}, l.hello)
l.connect()
b.notify('foo')
b.notify('bar')
b.notify('hello') # Normal dispatching still work
eq_(l.hello_count, 3)

209
hscommon/tests/path_test.py Normal file
View File

@ -0,0 +1,209 @@
# Created By: Virgil Dupras
# Created On: 2006/02/21
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
import sys
from pytest import raises, mark
from ..path import *
from ..testutil import eq_
def pytest_funcarg__force_ossep(request):
monkeypatch = request.getfuncargvalue('monkeypatch')
monkeypatch.setattr(os, 'sep', '/')
def test_empty(force_ossep):
path = Path('')
eq_('',str(path))
eq_(0,len(path))
path = Path(())
eq_('',str(path))
eq_(0,len(path))
def test_single(force_ossep):
path = Path('foobar')
eq_('foobar',path)
eq_(1,len(path))
def test_multiple(force_ossep):
path = Path('foo/bar')
eq_('foo/bar',path)
eq_(2,len(path))
def test_init_with_tuple_and_list(force_ossep):
path = Path(('foo','bar'))
eq_('foo/bar',path)
path = Path(['foo','bar'])
eq_('foo/bar',path)
def test_init_with_invalid_value(force_ossep):
try:
path = Path(42)
self.fail()
except TypeError:
pass
def test_access(force_ossep):
path = Path('foo/bar/bleh')
eq_('foo',path[0])
eq_('foo',path[-3])
eq_('bar',path[1])
eq_('bar',path[-2])
eq_('bleh',path[2])
eq_('bleh',path[-1])
def test_slicing(force_ossep):
path = Path('foo/bar/bleh')
subpath = path[:2]
eq_('foo/bar',subpath)
assert isinstance(subpath,Path)
def test_deal_with_empty_components(force_ossep):
"""Keep ONLY a leading space, which means we want a leading slash.
"""
eq_('foo//bar',str(Path(('foo','','bar'))))
eq_('/foo/bar',str(Path(('','foo','bar'))))
eq_('foo/bar',str(Path('foo/bar/')))
def test_old_compare_paths(force_ossep):
eq_(Path('foobar'),Path('foobar'))
eq_(Path('foobar/'),Path('foobar\\','\\'))
eq_(Path('/foobar/'),Path('\\foobar\\','\\'))
eq_(Path('/foo/bar'),Path('\\foo\\bar','\\'))
eq_(Path('/foo/bar'),Path('\\foo\\bar\\','\\'))
assert Path('/foo/bar') != Path('\\foo\\foo','\\')
#We also have to test __ne__
assert not (Path('foobar') != Path('foobar'))
assert Path('/a/b/c.x') != Path('/a/b/c.y')
def test_old_split_path(force_ossep):
eq_(Path('foobar'),('foobar',))
eq_(Path('foo/bar'),('foo','bar'))
eq_(Path('/foo/bar/'),('','foo','bar'))
eq_(Path('\\foo\\bar','\\'),('','foo','bar'))
def test_representation(force_ossep):
eq_("('foo', 'bar')",repr(Path(('foo','bar'))))
def test_add(force_ossep):
eq_('foo/bar/bar/foo',Path(('foo','bar')) + Path('bar/foo'))
eq_('foo/bar/bar/foo',Path('foo/bar') + 'bar/foo')
eq_('foo/bar/bar/foo',Path('foo/bar') + ('bar','foo'))
eq_('foo/bar/bar/foo',('foo','bar') + Path('bar/foo'))
eq_('foo/bar/bar/foo','foo/bar' + Path('bar/foo'))
#Invalid concatenation
try:
Path(('foo','bar')) + 1
self.fail()
except TypeError:
pass
def test_path_slice(force_ossep):
foo = Path('foo')
bar = Path('bar')
foobar = Path('foo/bar')
eq_('bar',foobar[foo:])
eq_('foo',foobar[:bar])
eq_('foo/bar',foobar[bar:])
eq_('foo/bar',foobar[:foo])
eq_((),foobar[foobar:])
eq_((),foobar[:foobar])
abcd = Path('a/b/c/d')
a = Path('a')
b = Path('b')
c = Path('c')
d = Path('d')
z = Path('z')
eq_('b/c',abcd[a:d])
eq_('b/c/d',abcd[a:d+z])
eq_('b/c',abcd[a:z+d])
eq_('a/b/c/d',abcd[:z])
def test_add_with_root_path(force_ossep):
"""if I perform /a/b/c + /d/e/f, I want /a/b/c/d/e/f, not /a/b/c//d/e/f
"""
eq_('/foo/bar',str(Path('/foo') + Path('/bar')))
def test_create_with_tuple_that_have_slash_inside(force_ossep, monkeypatch):
eq_(('','foo','bar'), Path(('/foo','bar')))
monkeypatch.setattr(os, 'sep', '\\')
eq_(('','foo','bar'), Path(('\\foo','bar')))
def test_auto_decode_os_sep(force_ossep, monkeypatch):
"""Path should decode any either / or os.sep, but always encode in os.sep.
"""
eq_(('foo\\bar','bleh'),Path('foo\\bar/bleh'))
monkeypatch.setattr(os, 'sep', '\\')
eq_(('foo','bar/bleh'),Path('foo\\bar/bleh'))
path = Path('foo/bar')
eq_(('foo','bar'),path)
eq_('foo\\bar',str(path))
def test_contains(force_ossep):
p = Path(('foo','bar'))
assert Path(('foo','bar','bleh')) in p
assert Path(('foo','bar')) in p
assert 'foo' in p
assert 'bleh' not in p
assert Path('foo') not in p
def test_windows_drive_letter(force_ossep):
p = Path(('c:',))
eq_('c:\\',str(p))
def test_root_path(force_ossep):
p = Path('/')
eq_('/',str(p))
def test_str_encodes_unicode_to_getfilesystemencoding(force_ossep):
p = Path(('foo','bar\u00e9'))
eq_('foo/bar\u00e9'.encode(sys.getfilesystemencoding()), p.tobytes())
def test_unicode(force_ossep):
p = Path(('foo','bar\u00e9'))
eq_('foo/bar\u00e9',str(p))
def test_str_repr_of_mix_between_non_ascii_str_and_unicode(force_ossep):
u = 'foo\u00e9'
encoded = u.encode(sys.getfilesystemencoding())
p = Path((encoded,'bar'))
print(repr(tuple(p)))
eq_('foo\u00e9/bar'.encode(sys.getfilesystemencoding()), p.tobytes())
def test_Path_of_a_Path_returns_self(force_ossep):
#if Path() is called with a path as value, just return value.
p = Path('foo/bar')
assert Path(p) is p
@mark.xfail(reason="pytest's capture mechanism is flaky, I have to investigate")
def test_log_unicode_errors(force_ossep, monkeypatch, capsys):
# When an there's a UnicodeDecodeError on path creation, log it so it can be possible
# to debug the cause of it.
monkeypatch.setattr(sys, 'getfilesystemencoding', lambda: 'ascii')
with raises(UnicodeDecodeError):
Path(['', b'foo\xe9'])
out, err = capsys.readouterr()
assert repr(b'foo\xe9') in err
def test_has_drive_letter(monkeypatch):
monkeypatch.setattr(os, 'sep', '\\')
p = Path('foo\\bar')
assert not p.has_drive_letter()
p = Path('C:\\')
assert p.has_drive_letter()
p = Path('z:\\foo')
assert p.has_drive_letter()
def test_remove_drive_letter(monkeypatch):
monkeypatch.setattr(os, 'sep', '\\')
p = Path('foo\\bar')
eq_(p.remove_drive_letter(), Path('foo\\bar'))
p = Path('C:\\')
eq_(p.remove_drive_letter(), Path(''))
p = Path('z:\\foo')
eq_(p.remove_drive_letter(), Path('foo'))

View File

@ -0,0 +1,68 @@
# Created By: Virgil Dupras
# Created On: 2010-01-31
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
from hashlib import md5
from ..testutil import CallLogger
from ..reg import RegistrableApplication, InvalidCodeError
def md5s(s):
return md5(s.encode('utf-8')).hexdigest()
def assert_valid(appid, code, email):
app = RegistrableApplication(CallLogger(), appid)
try:
app.validate_code(code, email)
except InvalidCodeError as e:
raise AssertionError("Registration failed: {0}".format(str(e)))
def assert_invalid(appid, code, email, msg_contains=None):
app = RegistrableApplication(CallLogger(), appid)
try:
app.validate_code(code, email)
except InvalidCodeError as e:
if msg_contains:
assert msg_contains in str(e)
else:
raise AssertionError("InvalidCodeError not raised")
def test_valid_code():
email = 'foo@bar.com'
appid = 42
code = md5s('42' + email + '43' + 'aybabtu')
assert_valid(appid, code, email)
def test_invalid_code():
email = 'foo@bar.com'
appid = 42
code = md5s('43' + email + '43' + 'aybabtu')
assert_invalid(appid, code, email)
def test_suggest_other_apps():
# If a code is valid for another app, say so in the error message.
email = 'foo@bar.com'
appid = 42
# 2 is moneyGuru's appid
code = md5s('2' + email + '43' + 'aybabtu')
assert_invalid(appid, code, email, msg_contains="moneyGuru")
def test_invert_code_and_email():
# Try inverting code and email during validation in case the user mixed the fields up.
# We still show an error here. It kind of sucks, but if we don't, the email and code fields
# end up mixed up in the preferences. It's not as if this kind of error happened all the time...
email = 'foo@bar.com'
appid = 42
code = md5s('42' + email + '43' + 'aybabtu')
assert_invalid(appid, email, code, msg_contains="inverted")
def test_paypal_transaction():
# If the code looks like a paypal transaction, mention it in the error message.
email = 'foo@bar.com'
appid = 42
code = '2A693827WX9676888'
assert_invalid(appid, code, email, 'Paypal transaction')

View File

@ -0,0 +1,65 @@
# Created By: Virgil Dupras
# Created On: 2011-09-06
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
from ..testutil import eq_, callcounter, CallLogger
from ..gui.selectable_list import SelectableList, GUISelectableList
def test_in():
# When a SelectableList is in a list, doing "in list" with another instance returns false, even
# if they're the same as lists.
sl = SelectableList()
some_list = [sl]
assert SelectableList() not in some_list
def test_selection_range():
# selection is correctly adjusted on deletion
sl = SelectableList(['foo', 'bar', 'baz'])
sl.selected_index = 3
eq_(sl.selected_index, 2)
del sl[2]
eq_(sl.selected_index, 1)
def test_update_selection_called():
# _update_selection_is called after a change in selection. However, we only do so on select()
# calls. I follow the old behavior of the Table class. At the moment, I don't quite remember
# why there was a specific select() method for triggering _update_selection(), but I think I
# remember there was a reason, so I keep it that way.
sl = SelectableList(['foo', 'bar'])
sl._update_selection = callcounter()
sl.select(1)
eq_(sl._update_selection.callcount, 1)
sl.selected_index = 0
eq_(sl._update_selection.callcount, 1) # no call
def test_guicalls():
# A GUISelectableList appropriately calls its view.
sl = GUISelectableList(['foo', 'bar'])
sl.view = CallLogger()
sl.view.check_gui_calls(['refresh']) # Upon setting the view, we get a call to refresh()
sl[1] = 'baz'
sl.view.check_gui_calls(['refresh'])
sl.append('foo')
sl.view.check_gui_calls(['refresh'])
del sl[2]
sl.view.check_gui_calls(['refresh'])
sl.remove('baz')
sl.view.check_gui_calls(['refresh'])
sl.insert(0, 'foo')
sl.view.check_gui_calls(['refresh'])
sl.select(1)
sl.view.check_gui_calls(['update_selection'])
# XXX We have to give up on this for now because of a breakage it causes in the tables.
# sl.select(1) # don't update when selection stays the same
# gui.check_gui_calls([])
def test_search_by_prefix():
sl = SelectableList(['foo', 'bAr', 'baZ'])
eq_(sl.search_by_prefix('b'), 1)
eq_(sl.search_by_prefix('BA'), 1)
eq_(sl.search_by_prefix('BAZ'), 2)
eq_(sl.search_by_prefix('BAZZ'), -1)

View File

@ -0,0 +1,126 @@
# Created By: Virgil Dupras
# Created On: 2007/05/19
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
import time
import threading
import os
import sqlite3 as sqlite
from pytest import raises
from ..testutil import eq_
from ..sqlite import ThreadedConn
# Threading is hard to test. In a lot of those tests, a failure means that the test run will
# hang forever. Well... I don't know a better alternative.
def test_can_access_from_multiple_threads():
def run():
con.execute('insert into foo(bar) values(\'baz\')')
con = ThreadedConn(':memory:', True)
con.execute('create table foo(bar TEXT)')
t = threading.Thread(target=run)
t.start()
t.join()
result = con.execute('select * from foo')
eq_(1, len(result))
eq_('baz', result[0][0])
def test_exception_during_query():
con = ThreadedConn(':memory:', True)
con.execute('create table foo(bar TEXT)')
with raises(sqlite.OperationalError):
con.execute('select * from bleh')
def test_not_autocommit(tmpdir):
dbpath = str(tmpdir.join('foo.db'))
con = ThreadedConn(dbpath, False)
con.execute('create table foo(bar TEXT)')
con.execute('insert into foo(bar) values(\'baz\')')
del con
#The data shouldn't have been inserted
con = ThreadedConn(dbpath, False)
result = con.execute('select * from foo')
eq_(0, len(result))
con.execute('insert into foo(bar) values(\'baz\')')
con.commit()
del con
# Now the data should be there
con = ThreadedConn(dbpath, False)
result = con.execute('select * from foo')
eq_(1, len(result))
def test_rollback():
con = ThreadedConn(':memory:', False)
con.execute('create table foo(bar TEXT)')
con.execute('insert into foo(bar) values(\'baz\')')
con.rollback()
result = con.execute('select * from foo')
eq_(0, len(result))
def test_query_palceholders():
con = ThreadedConn(':memory:', True)
con.execute('create table foo(bar TEXT)')
con.execute('insert into foo(bar) values(?)', ['baz'])
result = con.execute('select * from foo')
eq_(1, len(result))
eq_('baz', result[0][0])
def test_make_sure_theres_no_messup_between_queries():
def run(expected_rowid):
time.sleep(0.1)
result = con.execute('select rowid from foo where rowid = ?', [expected_rowid])
assert expected_rowid == result[0][0]
con = ThreadedConn(':memory:', True)
con.execute('create table foo(bar TEXT)')
for i in range(100):
con.execute('insert into foo(bar) values(\'baz\')')
threads = []
for i in range(1, 101):
t = threading.Thread(target=run, args=(i,))
t.start
threads.append(t)
while threads:
time.sleep(0.1)
threads = [t for t in threads if t.isAlive()]
def test_query_after_close():
con = ThreadedConn(':memory:', True)
con.close()
con.execute('select 1')
def test_lastrowid():
# It's not possible to return a cursor because of the threading, but lastrowid should be
# fetchable from the connection itself
con = ThreadedConn(':memory:', True)
con.execute('create table foo(bar TEXT)')
con.execute('insert into foo(bar) values(\'baz\')')
eq_(1, con.lastrowid)
def test_add_fetchone_fetchall_interface_to_results():
con = ThreadedConn(':memory:', True)
con.execute('create table foo(bar TEXT)')
con.execute('insert into foo(bar) values(\'baz1\')')
con.execute('insert into foo(bar) values(\'baz2\')')
result = con.execute('select * from foo')
ref = result[:]
eq_(ref, result.fetchall())
eq_(ref[0], result.fetchone())
eq_(ref[1], result.fetchone())
assert result.fetchone() is None
def test_non_ascii_dbname(tmpdir):
ThreadedConn(str(tmpdir.join('foo\u00e9.db')), True)
def test_non_ascii_dbdir(tmpdir):
# when this test fails, it doesn't fail gracefully, it brings the whole test suite with it.
dbdir = tmpdir.join('foo\u00e9')
os.mkdir(str(dbdir))
ThreadedConn(str(dbdir.join('foo.db')), True)

View File

@ -0,0 +1,313 @@
# Created By: Virgil Dupras
# Created On: 2008-08-12
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
from ..testutil import CallLogger, eq_
from ..gui.table import Table, GUITable, Row
class TestRow(Row):
def __init__(self, table, index, is_new=False):
Row.__init__(self, table)
self.is_new = is_new
self._index = index
def load(self):
pass
def save(self):
self.is_new = False
@property
def index(self):
return self._index
class TestGUITable(GUITable):
def __init__(self, rowcount):
GUITable.__init__(self)
self.view = CallLogger()
self.rowcount = rowcount
self.updated_rows = None
def _do_add(self):
return TestRow(self, len(self), is_new=True), len(self)
def _is_edited_new(self):
return self.edited is not None and self.edited.is_new
def _fill(self):
for i in range(self.rowcount):
self.append(TestRow(self, i))
def _update_selection(self):
self.updated_rows = self.selected_rows[:]
def table_with_footer():
table = Table()
table.append(TestRow(table, 0))
footer = TestRow(table, 1)
table.footer = footer
return table, footer
def table_with_header():
table = Table()
table.append(TestRow(table, 1))
header = TestRow(table, 0)
table.header = header
return table, header
#--- Tests
def test_allow_edit_when_attr_is_property_with_fset():
# When a row has a property that has a fset, by default, make that cell editable.
class TestRow(Row):
@property
def foo(self):
pass
@property
def bar(self):
pass
@bar.setter
def bar(self, value):
pass
row = TestRow(Table())
assert row.can_edit_cell('bar')
assert not row.can_edit_cell('foo')
assert not row.can_edit_cell('baz') # doesn't exist, can't edit
def test_can_edit_prop_has_priority_over_fset_checks():
# When a row has a cen_edit_* property, it's the result of that property that is used, not the
# result of a fset check.
class TestRow(Row):
@property
def bar(self):
pass
@bar.setter
def bar(self, value):
pass
can_edit_bar = False
row = TestRow(Table())
assert not row.can_edit_cell('bar')
def test_in():
# When a table is in a list, doing "in list" with another instance returns false, even if
# they're the same as lists.
table = Table()
some_list = [table]
assert Table() not in some_list
def test_footer_del_all():
# Removing all rows doesn't crash when doing the footer check.
table, footer = table_with_footer()
del table[:]
assert table.footer is None
def test_footer_del_row():
# Removing the footer row sets it to None
table, footer = table_with_footer()
del table[-1]
assert table.footer is None
eq_(len(table), 1)
def test_footer_is_appened_to_table():
# A footer is appended at the table's bottom
table, footer = table_with_footer()
eq_(len(table), 2)
assert table[1] is footer
def test_footer_remove():
# remove() on footer sets it to None
table, footer = table_with_footer()
table.remove(footer)
assert table.footer is None
def test_footer_replaces_old_footer():
table, footer = table_with_footer()
other = Row(table)
table.footer = other
assert table.footer is other
eq_(len(table), 2)
assert table[1] is other
def test_footer_rows_and_row_count():
# rows() and row_count() ignore footer.
table, footer = table_with_footer()
eq_(table.row_count, 1)
eq_(table.rows, table[:-1])
def test_footer_setting_to_none_removes_old_one():
table, footer = table_with_footer()
table.footer = None
assert table.footer is None
eq_(len(table), 1)
def test_footer_stays_there_on_append():
# Appending another row puts it above the footer
table, footer = table_with_footer()
table.append(Row(table))
eq_(len(table), 3)
assert table[2] is footer
def test_footer_stays_there_on_insert():
# Inserting another row puts it above the footer
table, footer = table_with_footer()
table.insert(3, Row(table))
eq_(len(table), 3)
assert table[2] is footer
def test_header_del_all():
# Removing all rows doesn't crash when doing the header check.
table, header = table_with_header()
del table[:]
assert table.header is None
def test_header_del_row():
# Removing the header row sets it to None
table, header = table_with_header()
del table[0]
assert table.header is None
eq_(len(table), 1)
def test_header_is_inserted_in_table():
# A header is inserted at the table's top
table, header = table_with_header()
eq_(len(table), 2)
assert table[0] is header
def test_header_remove():
# remove() on header sets it to None
table, header = table_with_header()
table.remove(header)
assert table.header is None
def test_header_replaces_old_header():
table, header = table_with_header()
other = Row(table)
table.header = other
assert table.header is other
eq_(len(table), 2)
assert table[0] is other
def test_header_rows_and_row_count():
# rows() and row_count() ignore header.
table, header = table_with_header()
eq_(table.row_count, 1)
eq_(table.rows, table[1:])
def test_header_setting_to_none_removes_old_one():
table, header = table_with_header()
table.header = None
assert table.header is None
eq_(len(table), 1)
def test_header_stays_there_on_insert():
# Inserting another row at the top puts it below the header
table, header = table_with_header()
table.insert(0, Row(table))
eq_(len(table), 3)
assert table[0] is header
def test_refresh_view_on_refresh():
# If refresh_view is not False, we refresh the table's view on refresh()
table = TestGUITable(1)
table.refresh()
table.view.check_gui_calls(['refresh'])
table.view.clear_calls()
table.refresh(refresh_view=False)
table.view.check_gui_calls([])
def test_restore_selection():
# By default, after a refresh, selection goes on the last row
table = TestGUITable(10)
table.refresh()
eq_(table.selected_indexes, [9])
def test_restore_selection_after_cancel_edits():
# _restore_selection() is called after cancel_edits(). Previously, only _update_selection would
# be called.
class MyTable(TestGUITable):
def _restore_selection(self, previous_selection):
self.selected_indexes = [6]
table = MyTable(10)
table.refresh()
table.add()
table.cancel_edits()
eq_(table.selected_indexes, [6])
def test_restore_selection_with_previous_selection():
# By default, we try to restore the selection that was there before a refresh
table = TestGUITable(10)
table.refresh()
table.selected_indexes = [2, 4]
table.refresh()
eq_(table.selected_indexes, [2, 4])
def test_restore_selection_custom():
# After a _fill() called, the virtual _restore_selection() is called so that it's possible for a
# GUITable subclass to customize its post-refresh selection behavior.
class MyTable(TestGUITable):
def _restore_selection(self, previous_selection):
self.selected_indexes = [6]
table = MyTable(10)
table.refresh()
eq_(table.selected_indexes, [6])
def test_row_cell_value():
# *_cell_value() correctly mangles attrnames that are Python reserved words.
row = Row(Table())
row.from_ = 'foo'
eq_(row.get_cell_value('from'), 'foo')
row.set_cell_value('from', 'bar')
eq_(row.get_cell_value('from'), 'bar')
def test_sort_table_also_tries_attributes_without_underscores():
# When determining a sort key, after having unsuccessfully tried the attribute with the,
# underscore, try the one without one.
table = Table()
row1 = Row(table)
row1._foo = 'a' # underscored attr must be checked first
row1.foo = 'b'
row1.bar = 'c'
row2 = Row(table)
row2._foo = 'b'
row2.foo = 'a'
row2.bar = 'b'
table.append(row1)
table.append(row2)
table.sort_by('foo')
assert table[0] is row1
assert table[1] is row2
table.sort_by('bar')
assert table[0] is row2
assert table[1] is row1
def test_sort_table_updates_selection():
table = TestGUITable(10)
table.refresh()
table.select([2, 4])
table.sort_by('index', desc=True)
# Now, the updated rows should be 7 and 5
eq_(len(table.updated_rows), 2)
r1, r2 = table.updated_rows
eq_(r1.index, 7)
eq_(r2.index, 5)
def test_sort_table_with_footer():
# Sorting a table with a footer keeps it at the bottom
table, footer = table_with_footer()
table.sort_by('index', desc=True)
assert table[-1] is footer
def test_sort_table_with_header():
# Sorting a table with a header keeps it at the top
table, header = table_with_header()
table.sort_by('index', desc=True)
assert table[0] is header

109
hscommon/tests/tree_test.py Normal file
View File

@ -0,0 +1,109 @@
# Created By: Virgil Dupras
# Created On: 2010-02-12
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
from ..testutil import eq_
from ..gui.tree import Tree, Node
def tree_with_some_nodes():
t = Tree()
t.append(Node('foo'))
t.append(Node('bar'))
t.append(Node('baz'))
t[0].append(Node('sub1'))
t[0].append(Node('sub2'))
return t
def test_selection():
t = tree_with_some_nodes()
assert t.selected_node is None
eq_(t.selected_nodes, [])
assert t.selected_path is None
eq_(t.selected_paths, [])
def test_select_one_node():
t = tree_with_some_nodes()
t.selected_node = t[0][0]
assert t.selected_node is t[0][0]
eq_(t.selected_nodes, [t[0][0]])
eq_(t.selected_path, [0, 0])
eq_(t.selected_paths, [[0, 0]])
def test_select_one_path():
t = tree_with_some_nodes()
t.selected_path = [0, 1]
assert t.selected_node is t[0][1]
def test_select_multiple_nodes():
t = tree_with_some_nodes()
t.selected_nodes = [t[0], t[1]]
eq_(t.selected_paths, [[0], [1]])
def test_select_multiple_paths():
t = tree_with_some_nodes()
t.selected_paths = [[0], [1]]
eq_(t.selected_nodes, [t[0], t[1]])
def test_select_none_path():
# setting selected_path to None clears the selection
t = Tree()
t.selected_path = None
assert t.selected_path is None
def test_select_none_node():
# setting selected_node to None clears the selection
t = Tree()
t.selected_node = None
eq_(t.selected_nodes, [])
def test_clear_removes_selection():
# When clearing a tree, we want to clear the selection as well or else we end up with a crash
# when calling selected_paths.
t = tree_with_some_nodes()
t.selected_path = [0]
t.clear()
assert t.selected_node is None
def test_selection_override():
# All selection changed pass through the _select_node() method so it's easy for subclasses to
# customize the tree's behavior.
class MyTree(Tree):
called = False
def _select_nodes(self, nodes):
self.called = True
t = MyTree()
t.selected_paths = []
assert t.called
t.called = False
t.selected_node = None
assert t.called
def test_findall():
t = tree_with_some_nodes()
r = t.findall(lambda n: n.name.startswith('sub'))
eq_(set(r), set([t[0][0], t[0][1]]))
def test_findall_dont_include_self():
# When calling findall with include_self=False, the node itself is never evaluated.
t = tree_with_some_nodes()
del t._name # so that if the predicate is called on `t`, we crash
r = t.findall(lambda n: not n.name.startswith('sub'), include_self=False) # no crash
eq_(set(r), set([t[0], t[1], t[2]]))
def test_find_dont_include_self():
# When calling find with include_self=False, the node itself is never evaluated.
t = tree_with_some_nodes()
del t._name # so that if the predicate is called on `t`, we crash
r = t.find(lambda n: not n.name.startswith('sub'), include_self=False) # no crash
assert r is t[0]
def test_find_none():
# when find() yields no result, return None
t = Tree()
assert t.find(lambda n: False) is None # no StopIteration exception

310
hscommon/tests/util_test.py Normal file
View File

@ -0,0 +1,310 @@
# Created By: Virgil Dupras
# Created On: 2011-01-11
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
from io import StringIO
from pytest import raises
from ..testutil import eq_
from .. import io
from ..path import Path
from ..util import *
def test_nonone():
eq_('foo', nonone('foo', 'bar'))
eq_('bar', nonone(None, 'bar'))
def test_tryint():
eq_(42,tryint('42'))
eq_(0,tryint('abc'))
eq_(0,tryint(None))
eq_(42,tryint(None, 42))
def test_minmax():
eq_(minmax(2, 1, 3), 2)
eq_(minmax(0, 1, 3), 1)
eq_(minmax(4, 1, 3), 3)
#--- Sequence
def test_first():
eq_(first([3, 2, 1]), 3)
eq_(first(i for i in [3, 2, 1] if i < 3), 2)
def test_flatten():
eq_([1,2,3,4],flatten([[1,2],[3,4]]))
eq_([],flatten([]))
def test_dedupe():
reflist = [0,7,1,2,3,4,4,5,6,7,1,2,3]
eq_(dedupe(reflist),[0,7,1,2,3,4,5,6])
def test_stripfalse():
eq_([1, 2, 3], stripfalse([None, 0, 1, 2, 3, None]))
def test_extract():
wheat, shaft = extract(lambda n: n % 2 == 0, list(range(10)))
eq_(wheat, [0, 2, 4, 6, 8])
eq_(shaft, [1, 3, 5, 7, 9])
def test_allsame():
assert allsame([42, 42, 42])
assert not allsame([42, 43, 42])
assert not allsame([43, 42, 42])
# Works on non-sequence as well
assert allsame(iter([42, 42, 42]))
def test_trailiter():
eq_(list(trailiter([])), [])
eq_(list(trailiter(['foo'])), [(None, 'foo')])
eq_(list(trailiter(['foo', 'bar'])), [(None, 'foo'), ('foo', 'bar')])
eq_(list(trailiter(['foo', 'bar'], skipfirst=True)), [('foo', 'bar')])
eq_(list(trailiter([], skipfirst=True)), []) # no crash
#--- String
def test_escape():
eq_('f\\o\\ob\\ar', escape('foobar', 'oa'))
eq_('f*o*ob*ar', escape('foobar', 'oa', '*'))
eq_('f*o*ob*ar', escape('foobar', set('oa'), '*'))
def test_get_file_ext():
eq_(get_file_ext("foobar"), "")
eq_(get_file_ext("foo.bar"), "bar")
eq_(get_file_ext("foobar."), "")
eq_(get_file_ext(".foobar"), "foobar")
def test_rem_file_ext():
eq_(rem_file_ext("foobar"), "foobar")
eq_(rem_file_ext("foo.bar"), "foo")
eq_(rem_file_ext("foobar."), "foobar")
eq_(rem_file_ext(".foobar"), "")
def test_pluralize():
eq_('0 song', pluralize(0,'song'))
eq_('1 song', pluralize(1,'song'))
eq_('2 songs', pluralize(2,'song'))
eq_('1 song', pluralize(1.1,'song'))
eq_('2 songs', pluralize(1.5,'song'))
eq_('1.1 songs', pluralize(1.1,'song',1))
eq_('1.5 songs', pluralize(1.5,'song',1))
eq_('2 entries', pluralize(2,'entry', plural_word='entries'))
def test_format_time():
eq_(format_time(0),'00:00:00')
eq_(format_time(1),'00:00:01')
eq_(format_time(23),'00:00:23')
eq_(format_time(60),'00:01:00')
eq_(format_time(101),'00:01:41')
eq_(format_time(683),'00:11:23')
eq_(format_time(3600),'01:00:00')
eq_(format_time(3754),'01:02:34')
eq_(format_time(36000),'10:00:00')
eq_(format_time(366666),'101:51:06')
eq_(format_time(0, with_hours=False),'00:00')
eq_(format_time(1, with_hours=False),'00:01')
eq_(format_time(23, with_hours=False),'00:23')
eq_(format_time(60, with_hours=False),'01:00')
eq_(format_time(101, with_hours=False),'01:41')
eq_(format_time(683, with_hours=False),'11:23')
eq_(format_time(3600, with_hours=False),'60:00')
eq_(format_time(6036, with_hours=False),'100:36')
eq_(format_time(60360, with_hours=False),'1006:00')
def test_format_time_decimal():
eq_(format_time_decimal(0), '0.0 second')
eq_(format_time_decimal(1), '1.0 second')
eq_(format_time_decimal(23), '23.0 seconds')
eq_(format_time_decimal(60), '1.0 minute')
eq_(format_time_decimal(101), '1.7 minutes')
eq_(format_time_decimal(683), '11.4 minutes')
eq_(format_time_decimal(3600), '1.0 hour')
eq_(format_time_decimal(6036), '1.7 hours')
eq_(format_time_decimal(86400), '1.0 day')
eq_(format_time_decimal(160360), '1.9 days')
def test_format_size():
eq_(format_size(1024), '1 KB')
eq_(format_size(1024,2), '1.00 KB')
eq_(format_size(1024,0,2), '1 MB')
eq_(format_size(1024,2,2), '0.01 MB')
eq_(format_size(1024,3,2), '0.001 MB')
eq_(format_size(1024,3,2,False), '0.001')
eq_(format_size(1023), '1023 B')
eq_(format_size(1023,0,1), '1 KB')
eq_(format_size(511,0,1), '1 KB')
eq_(format_size(9), '9 B')
eq_(format_size(99), '99 B')
eq_(format_size(999), '999 B')
eq_(format_size(9999), '10 KB')
eq_(format_size(99999), '98 KB')
eq_(format_size(999999), '977 KB')
eq_(format_size(9999999), '10 MB')
eq_(format_size(99999999), '96 MB')
eq_(format_size(999999999), '954 MB')
eq_(format_size(9999999999), '10 GB')
eq_(format_size(99999999999), '94 GB')
eq_(format_size(999999999999), '932 GB')
eq_(format_size(9999999999999), '10 TB')
eq_(format_size(99999999999999), '91 TB')
eq_(format_size(999999999999999), '910 TB')
eq_(format_size(9999999999999999), '9 PB')
eq_(format_size(99999999999999999), '89 PB')
eq_(format_size(999999999999999999), '889 PB')
eq_(format_size(9999999999999999999), '9 EB')
eq_(format_size(99999999999999999999), '87 EB')
eq_(format_size(999999999999999999999), '868 EB')
eq_(format_size(9999999999999999999999), '9 ZB')
eq_(format_size(99999999999999999999999), '85 ZB')
eq_(format_size(999999999999999999999999), '848 ZB')
def test_remove_invalid_xml():
eq_(remove_invalid_xml('foo\0bar\x0bbaz'), 'foo bar baz')
# surrogate blocks have to be replaced, but not the rest
eq_(remove_invalid_xml('foo\ud800bar\udfffbaz\ue000'), 'foo bar baz\ue000')
# replace with something else
eq_(remove_invalid_xml('foo\0baz', replace_with='bar'), 'foobarbaz')
def test_multi_replace():
eq_('136',multi_replace('123456',('2','45')))
eq_('1 3 6',multi_replace('123456',('2','45'),' '))
eq_('1 3 6',multi_replace('123456','245',' '))
eq_('173896',multi_replace('123456','245','789'))
eq_('173896',multi_replace('123456','245',('7','8','9')))
eq_('17386',multi_replace('123456',('2','45'),'78'))
eq_('17386',multi_replace('123456',('2','45'),('7','8')))
with raises(ValueError):
multi_replace('123456',('2','45'),('7','8','9'))
eq_('17346',multi_replace('12346',('2','45'),'78'))
#--- Files
class TestCase_modified_after:
def test_first_is_modified_after(self, monkeyplus):
monkeyplus.patch_osstat('first', st_mtime=42)
monkeyplus.patch_osstat('second', st_mtime=41)
assert modified_after('first', 'second')
def test_second_is_modified_after(self, monkeyplus):
monkeyplus.patch_osstat('first', st_mtime=42)
monkeyplus.patch_osstat('second', st_mtime=43)
assert not modified_after('first', 'second')
def test_same_mtime(self, monkeyplus):
monkeyplus.patch_osstat('first', st_mtime=42)
monkeyplus.patch_osstat('second', st_mtime=42)
assert not modified_after('first', 'second')
def test_first_file_does_not_exist(self, monkeyplus):
# when the first file doesn't exist, we return False
monkeyplus.patch_osstat('second', st_mtime=42)
assert not modified_after('does_not_exist', 'second') # no crash
def test_second_file_does_not_exist(self, monkeyplus):
# when the second file doesn't exist, we return True
monkeyplus.patch_osstat('first', st_mtime=42)
assert modified_after('first', 'does_not_exist') # no crash
class TestCase_delete_if_empty:
def test_is_empty(self, tmpdir):
testpath = Path(str(tmpdir))
assert delete_if_empty(testpath)
assert not io.exists(testpath)
def test_not_empty(self, tmpdir):
testpath = Path(str(tmpdir))
io.mkdir(testpath + 'foo')
assert not delete_if_empty(testpath)
assert io.exists(testpath)
def test_with_files_to_delete(self, tmpdir):
testpath = Path(str(tmpdir))
io.open(testpath + 'foo', 'w')
io.open(testpath + 'bar', 'w')
assert delete_if_empty(testpath, ['foo', 'bar'])
assert not io.exists(testpath)
def test_directory_in_files_to_delete(self, tmpdir):
testpath = Path(str(tmpdir))
io.mkdir(testpath + 'foo')
assert not delete_if_empty(testpath, ['foo'])
assert io.exists(testpath)
def test_delete_files_to_delete_only_if_dir_is_empty(self, tmpdir):
testpath = Path(str(tmpdir))
io.open(testpath + 'foo', 'w')
io.open(testpath + 'bar', 'w')
assert not delete_if_empty(testpath, ['foo'])
assert io.exists(testpath)
assert io.exists(testpath + 'foo')
def test_doesnt_exist(self):
# When the 'path' doesn't exist, just do nothing.
delete_if_empty(Path('does_not_exist')) # no crash
def test_is_file(self, tmpdir):
# When 'path' is a file, do nothing.
p = Path(str(tmpdir)) + 'filename'
io.open(p, 'w').close()
delete_if_empty(p) # no crash
def test_ioerror(self, tmpdir, monkeypatch):
# if an IO error happens during the operation, ignore it.
def do_raise(*args, **kw):
raise OSError()
monkeypatch.setattr(io, 'rmdir', do_raise)
delete_if_empty(Path(str(tmpdir))) # no crash
class TestCase_open_if_filename:
def test_file_name(self, tmpdir):
filepath = str(tmpdir.join('test.txt'))
open(filepath, 'wb').write(b'test_data')
file, close = open_if_filename(filepath)
assert close
eq_(b'test_data', file.read())
file.close()
def test_opened_file(self):
sio = StringIO()
sio.write('test_data')
sio.seek(0)
file, close = open_if_filename(sio)
assert not close
eq_('test_data', file.read())
def test_mode_is_passed_to_open(self, tmpdir):
filepath = str(tmpdir.join('test.txt'))
open(filepath, 'w').close()
file, close = open_if_filename(filepath, 'a')
eq_('a', file.mode)
file.close()
class TestCase_FileOrPath:
def test_path(self, tmpdir):
filepath = str(tmpdir.join('test.txt'))
open(filepath, 'wb').write(b'test_data')
with FileOrPath(filepath) as fp:
eq_(b'test_data', fp.read())
def test_opened_file(self):
sio = StringIO()
sio.write('test_data')
sio.seek(0)
with FileOrPath(sio) as fp:
eq_('test_data', fp.read())
def test_mode_is_passed_to_open(self, tmpdir):
filepath = str(tmpdir.join('test.txt'))
open(filepath, 'w').close()
with FileOrPath(filepath, 'a') as fp:
eq_('a', fp.mode)

212
hscommon/testutil.py Normal file
View File

@ -0,0 +1,212 @@
# Created By: Virgil Dupras
# Created On: 2010-11-14
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
import threading
import py.path
def eq_(a, b, msg=None):
__tracebackhide__ = True
assert a == b, msg or "%r != %r" % (a, b)
def assert_almost_equal(a, b, places=7):
__tracebackhide__ = True
assert round(a, ndigits=places) == round(b, ndigits=places)
def callcounter():
def f(*args, **kwargs):
f.callcount += 1
f.callcount = 0
return f
class TestData:
def __init__(self, datadirpath):
self.datadirpath = py.path.local(datadirpath)
def filepath(self, relative_path, *args):
"""Returns the path of a file in testdata.
'relative_path' can be anything that can be added to a Path
if args is not empty, it will be joined to relative_path
"""
resultpath = self.datadirpath.join(relative_path)
if args:
resultpath = resultpath.join(*args)
assert resultpath.check()
return str(resultpath)
class CallLogger:
"""This is a dummy object that logs all calls made to it.
It is used to simulate the GUI layer.
"""
def __init__(self):
self.calls = []
def __getattr__(self, func_name):
def func(*args, **kw):
self.calls.append(func_name)
return func
def clear_calls(self):
del self.calls[:]
def check_gui_calls(self, expected, verify_order=False):
"""Checks that the expected calls have been made to 'self', then clears the log.
`expected` is an iterable of strings representing method names.
If `verify_order` is True, the order of the calls matters.
"""
__tracebackhide__ = True
if verify_order:
eq_(self.calls, expected)
else:
eq_(set(self.calls), set(expected))
self.clear_calls()
def check_gui_calls_partial(self, expected=None, not_expected=None, verify_order=False):
"""Checks that the expected calls have been made to 'self', then clears the log.
`expected` is an iterable of strings representing method names. Order doesn't matter.
Moreover, if calls have been made that are not in expected, no failure occur.
`not_expected` can be used for a more explicit check (rather than calling `check_gui_calls`
with an empty `expected`) to assert that calls have *not* been made.
"""
__tracebackhide__ = True
if expected is not None:
not_called = set(expected) - set(self.calls)
assert not not_called, "These calls haven't been made: {0}".format(not_called)
if verify_order:
max_index = 0
for call in expected:
index = self.calls.index(call)
if index < max_index:
raise AssertionError("The call {0} hasn't been made in the correct order".format(call))
max_index = index
if not_expected is not None:
called = set(not_expected) & set(self.calls)
assert not called, "These calls shouldn't have been made: {0}".format(called)
self.clear_calls()
class TestApp:
def __init__(self):
self._call_loggers = []
def clear_gui_calls(self):
for logger in self._call_loggers:
logger.clear_calls()
def make_logger(self, class_=CallLogger, *initargs):
logger = class_(*initargs)
self._call_loggers.append(logger)
return logger
def make_gui(self, name, class_, view=None, parent=None, holder=None):
if view is None:
view = self.make_logger()
if parent is None:
# The attribute "default_parent" has to be set for this to work correctly
parent = self.default_parent
if holder is None:
holder = self
setattr(holder, '{0}_gui'.format(name), view)
gui = class_(parent)
gui.view = view
setattr(holder, name, gui)
return gui
# To use @with_app, you have to import pytest_funcarg__app in your conftest.py file.
def with_app(setupfunc):
def decorator(func):
func.setupfunc = setupfunc
return func
return decorator
def pytest_funcarg__app(request):
setupfunc = request.function.setupfunc
if hasattr(setupfunc, '__code__'):
argnames = setupfunc.__code__.co_varnames[:setupfunc.__code__.co_argcount]
def getarg(name):
if name == 'self':
return request.function.__self__
else:
return request.getfuncargvalue(name)
args = [getarg(argname) for argname in argnames]
else:
args = []
app = setupfunc(*args)
return app
def jointhreads():
"""Join all threads to the main thread"""
for thread in threading.enumerate():
if hasattr(thread, 'BUGGY'):
continue
if thread.getName() != 'MainThread' and thread.isAlive():
if hasattr(thread, 'close'):
thread.close()
thread.join(1)
if thread.isAlive():
print("Thread problem. Some thread doesn't want to stop.")
thread.BUGGY = True
def _unify_args(func, args, kwargs, args_to_ignore=None):
''' Unify args and kwargs in the same dictionary.
The result is kwargs with args added to it. func.func_code.co_varnames is used to determine
under what key each elements of arg will be mapped in kwargs.
if you want some arguments not to be in the results, supply a list of arg names in
args_to_ignore.
if f is a function that takes *args, func_code.co_varnames is empty, so args will be put
under 'args' in kwargs.
def foo(bar, baz)
_unifyArgs(foo, (42,), {'baz': 23}) --> {'bar': 42, 'baz': 23}
_unifyArgs(foo, (42,), {'baz': 23}, ['bar']) --> {'baz': 23}
'''
result = kwargs.copy()
if hasattr(func, '__code__'): # built-in functions don't have func_code
args = list(args)
if getattr(func, '__self__', None) is not None: # bound method, we have to add self to args list
args = [func.__self__] + args
defaults = list(func.__defaults__) if func.__defaults__ is not None else []
arg_count = func.__code__.co_argcount
arg_names = list(func.__code__.co_varnames)
if len(args) < arg_count: # We have default values
required_arg_count = arg_count - len(args)
args = args + defaults[-required_arg_count:]
for arg_name, arg in zip(arg_names, args):
# setdefault is used because if the arg is already in kwargs, we don't want to use default values
result.setdefault(arg_name, arg)
else:
#'func' has a *args argument
result['args'] = args
if args_to_ignore:
for kw in args_to_ignore:
del result[kw]
return result
def log_calls(func):
''' Logs all func calls' arguments under func.calls.
func.calls is a list of _unify_args() result (dict).
Mostly used for unit testing.
'''
def wrapper(*args, **kwargs):
unifiedArgs = _unify_args(func, args, kwargs)
wrapper.calls.append(unifiedArgs)
return func(*args, **kwargs)
wrapper.calls = []
return wrapper

138
hscommon/trans.py Normal file
View File

@ -0,0 +1,138 @@
# Created By: Virgil Dupras
# Created On: 2010-06-23
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
# Doing i18n with GNU gettext for the core text gets complicated, so what I do is that I make the
# GUI layer responsible for supplying a tr() function.
import locale
import logging
import os.path as op
from .plat import ISWINDOWS, ISLINUX
_trfunc = None
_trget = None
installed_lang = None
def tr(s, context=None):
if _trfunc is None:
return s
else:
if context:
return _trfunc(s, context)
else:
return _trfunc(s)
def trget(domain):
# Returns a tr() function for the specified domain.
if _trget is None:
return lambda s: tr(s, domain)
else:
return _trget(domain)
def set_tr(new_tr, new_trget=None):
global _trfunc, _trget
_trfunc = new_tr
if new_trget is not None:
_trget = new_trget
def get_locale_name(lang):
if ISWINDOWS:
# http://msdn.microsoft.com/en-us/library/39cwe7zf(vs.71).aspx
LANG2LOCALENAME = {'fr': 'fra', 'de': 'deu', 'it': 'ita', 'zh_CN': 'chs', 'cs': 'czy',
'nl': 'nld', 'ru': 'rus', 'pt_BR': 'ptb', 'es': 'esn'}
else:
LANG2LOCALENAME = {'fr': 'fr_FR', 'de': 'de_DE', 'it': 'it_IT', 'zh_CN': 'zh_CN',
'cs': 'cs_CZ', 'nl': 'nl_NL', 'hy': 'hy_AM', 'ru': 'ru_RU', 'uk': 'uk_UA',
'pt_BR': 'pt_BR', 'es': 'es_ES'}
if lang not in LANG2LOCALENAME:
return None
result = LANG2LOCALENAME[lang]
if ISLINUX:
result += '.UTF-8'
return result
#--- Qt
def install_qt_trans(lang=None):
from PyQt4.QtCore import QCoreApplication, QTranslator, QLocale
if not lang:
lang = str(QLocale.system().name())[:2]
localename = get_locale_name(lang)
if localename is not None:
try:
locale.setlocale(locale.LC_ALL, localename)
except locale.Error:
logging.warning("Couldn't set locale %s", localename)
else:
lang = 'en'
qtr1 = QTranslator(QCoreApplication.instance())
qtr1.load(':/qt_%s' % lang)
QCoreApplication.installTranslator(qtr1)
qtr2 = QTranslator(QCoreApplication.instance())
qtr2.load(':/%s' % lang)
QCoreApplication.installTranslator(qtr2)
def qt_tr(s, context='core'):
return str(QCoreApplication.translate(context, s, None))
set_tr(qt_tr)
#--- gettext
def install_gettext_trans(base_folder, lang):
import gettext
def gettext_trget(domain):
if not lang:
return lambda s: s
try:
return gettext.translation(domain, localedir=base_folder, languages=[lang]).gettext
except IOError:
return lambda s: s
default_gettext = gettext_trget('core')
def gettext_tr(s, context=None):
if not context:
return default_gettext(s)
else:
trfunc = gettext_trget(context)
return trfunc(s)
set_tr(gettext_tr, gettext_trget)
global installed_lang
installed_lang = lang
def install_gettext_trans_under_cocoa():
from cocoa import proxy
resFolder = proxy.getResourcePath()
baseFolder = op.join(resFolder, 'locale')
currentLang = proxy.systemLang()
install_gettext_trans(baseFolder, currentLang)
localename = get_locale_name(currentLang)
if localename is not None:
locale.setlocale(locale.LC_ALL, localename)
def install_gettext_trans_under_qt(base_folder, lang=None):
# So, we install the gettext locale, great, but we also should try to install qt_*.qm if
# available so that strings that are inside Qt itself over which I have no control are in the
# right language.
from PyQt4.QtCore import QCoreApplication, QTranslator, QLocale, QLibraryInfo
if not lang:
lang = str(QLocale.system().name())[:2]
localename = get_locale_name(lang)
if localename is not None:
try:
locale.setlocale(locale.LC_ALL, localename)
except locale.Error:
logging.warning("Couldn't set locale %s", localename)
qmname = 'qt_%s' % lang
if ISLINUX:
# Under linux, a full Qt installation is already available in the system, we didn't bundle
# up the qm files in our package, so we have to load translations from the system.
qmpath = op.join(QLibraryInfo.location(QLibraryInfo.TranslationsPath), qmname)
else:
qmpath = op.join(base_folder, qmname)
qtr = QTranslator(QCoreApplication.instance())
qtr.load(qmpath)
QCoreApplication.installTranslator(qtr)
install_gettext_trans(base_folder, lang)

350
hscommon/util.py Normal file
View File

@ -0,0 +1,350 @@
# Created By: Virgil Dupras
# Created On: 2011-01-11
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
import sys
import os
import os.path as op
import re
from math import ceil
import glob
import shutil
from . import io
from .path import Path
def nonone(value, replace_value):
''' 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):
try:
return int(value)
except (TypeError, ValueError):
return default
def minmax(value, min_value, max_value):
"""Returns `value` or one of the min/max bounds if `value` is not between them.
"""
return min(max(value, min_value), max_value)
#--- Sequence related
def dedupe(iterable):
'''Returns a list of elements in iterable with all dupes removed.
'''
result = []
seen = {}
for item in iterable:
if item in seen:
continue
seen[item] = 1
result.append(item)
return result
def flatten(iterables, start_with=None):
'''Takes a list of lists 'lists' 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.
'''
result = []
if start_with:
result.extend(start_with)
for iterable in iterables:
result.extend(iterable)
return result
def first(iterable):
"""Returns the first item of 'iterable'
"""
try:
return next(iter(iterable))
except StopIteration:
return None
def stripfalse(seq):
"""Returns a sequence with all false elements stripped out of seq.
"""
return [x for x in seq if x]
def extract(predicate, iterable):
"""Separates the wheat from the shaft (`predicate` defines what's the wheat), and returns both.
"""
wheat = []
shaft = []
for item in iterable:
if predicate(item):
wheat.append(item)
else:
shaft.append(item)
return wheat, shaft
def allsame(iterable):
"""Returns whether all elements of 'iterable' are the same.
"""
it = iter(iterable)
try:
first_item = next(it)
except StopIteration:
raise ValueError("iterable cannot be empty")
return all(element == first_item for element in it)
def trailiter(iterable, skipfirst=False):
"""Yields (prev_element, element), starting with (None, first_element).
If skipfirst is True, there will be no (None, item1) element and we'll start
directly with (item1, item2).
"""
it = iter(iterable)
if skipfirst:
prev = next(it)
else:
prev = None
for item in it:
yield prev, item
prev = item
#--- String related
def escape(s, to_escape, 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."""
pos = filename.rfind('.')
if pos > -1:
return filename[pos + 1:].lower()
else:
return ''
def rem_file_ext(filename):
"""Returns the filename without extension."""
pos = filename.rfind('.')
if pos > -1:
return filename[:pos]
else:
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
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
if number > 1:
if plural_word is None:
word += 's'
else:
word = plural_word
return format % (number, word)
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.
"""
minus = seconds < 0
if minus:
seconds *= -1
m, s = divmod(seconds, 60)
if with_hours:
h, m = divmod(m, 60)
r = '%02d:%02d:%02d' % (h, m, s)
else:
r = '%02d:%02d' % (m,s)
if minus:
return '-' + r
else:
return r
def format_time_decimal(seconds):
"""Transforms seconds in a strings like '3.4 minutes'"""
minus = seconds < 0
if minus:
seconds *= -1
if seconds < 60:
r = pluralize(seconds, 'second', 1)
elif seconds < 3600:
r = pluralize(seconds / 60.0, 'minute', 1)
elif seconds < 86400:
r = pluralize(seconds / 3600.0, 'hour', 1)
else:
r = pluralize(seconds / 86400.0, 'day', 1)
if minus:
return '-' + r
else:
return r
SIZE_DESC = ('B','KB','MB','GB','TB','PB','EB','ZB','YB')
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.
"""
if forcepower < 0:
i = 0
while size >= SIZE_VALS[i]:
i += 1
else:
i = forcepower
if i > 0:
div = SIZE_VALS[i-1]
else:
div = 1
format = '%%%d.%df' % (decimal,decimal)
negative = size < 0
divided_size = ((0.0 + abs(size)) / div)
if decimal == 0:
divided_size = ceil(divided_size)
else:
divided_size = ceil(divided_size * (10 ** decimal)) / (10 ** decimal)
if negative:
divided_size *= -1
result = format % divided_size
if showdesc:
result += ' ' + SIZE_DESC[i]
return result
_valid_xml_range = '\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD'
if sys.maxunicode > 0x10000:
_valid_xml_range += '%s-%s' % (chr(0x10000), chr(min(sys.maxunicode, 0x10FFFF)))
RE_INVALID_XML_SUB = re.compile('[^%s]' % _valid_xml_range, re.U).sub
def remove_invalid_xml(s, replace_with=' '):
return RE_INVALID_XML_SUB(replace_with, s)
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
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.
"""
if isinstance(replace_to, str) and (len(replace_from) != len(replace_to)):
replace_to = [replace_to for r in replace_from]
if len(replace_from) != len(replace_to):
raise ValueError('len(replace_from) must be equal to len(replace_to)')
replace = list(zip(replace_from, replace_to))
for r_from, r_to in [r for r in replace if r[0] in s]:
s = s.replace(r_from, r_to)
return s
#--- Files related
def modified_after(first_path, second_path):
"""Returns True if first_path's mtime is higher than second_path's mtime."""
try:
first_mtime = io.stat(first_path).st_mtime
except EnvironmentError:
return False
try:
second_mtime = io.stat(second_path).st_mtime
except EnvironmentError:
return True
return first_mtime > second_mtime
def find_in_path(name, paths=None):
"""Search for `name` in all directories of `paths` and return the absolute path of the first
occurrence. If `paths` is None, $PATH is used.
"""
if paths is None:
paths = os.environ['PATH']
if isinstance(paths, str): # if it's not a string, it's already a list
paths = paths.split(os.pathsep)
for path in paths:
if op.exists(op.join(path, name)):
return op.join(path, name)
return None
@io.log_io_error
def delete_if_empty(path, files_to_delete=[]):
''' Deletes the directory at 'path' if it is empty or if it only contains files_to_delete.
'''
if not io.exists(path) or not io.isdir(path):
return
contents = io.listdir(path)
if any(name for name in contents if (name not in files_to_delete) or io.isdir(path + name)):
return False
for name in contents:
io.remove(path + name)
io.rmdir(path)
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 isinstance(infile, Path):
return (io.open(infile, mode), True)
if isinstance(infile, str):
return (open(infile, mode), True)
else:
return (infile, False)
def ensure_folder(path):
"Create `path` as a folder if it doesn't exist."
if not op.exists(path):
os.makedirs(path)
def ensure_file(path):
"Create `path` as an empty file if it doesn't exist."
if not op.exists(path):
open(path, 'w').close()
def delete_files_with_pattern(folder_path, pattern, recursive=True):
"""Delete all files (or folders) in `folder_path` that match the glob `pattern`.
"""
to_delete = glob.glob(op.join(folder_path, pattern))
for fn in to_delete:
if op.isdir(fn):
shutil.rmtree(fn)
else:
os.remove(fn)
if recursive:
subpaths = [op.join(folder_path, fn) for fn in os.listdir(folder_path)]
subfolders = [p for p in subpaths if op.isdir(p)]
for p in subfolders:
delete_files_with_pattern(p, pattern, True)
class FileOrPath:
def __init__(self, file_or_path, mode='rb'):
self.file_or_path = file_or_path
self.mode = mode
self.mustclose = False
self.fp = None
def __enter__(self):
self.fp, self.mustclose = open_if_filename(self.file_or_path, self.mode)
return self.fp
def __exit__(self, exc_type, exc_value, traceback):
if self.fp and self.mustclose:
self.fp.close()