diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3d4d505204e049c862039cbce1b1bc2921687e6d..243db5f1ee6a57c7490e1f1b81814f36a8aaaa1d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,17 +1,380 @@ -test:2.7: - image: fsleyes-py27 +########################################################################### +# This file defines the build process for fslpy, as hosted at: +# +# https://git.fmrib.ox.ac.uk/fsl/fslpy +# +# The build pipeline comprises four stages: +# +# 1. test: Unit tests +# +# 2. doc: Building API documentation +# +# 3. build: Building source distributions and wheels +# +# 4. deploy: Uploading the build outputs to pypi, and the documentation +# to a hosting server. +# +# The test stage is executed on all branches of upstream and fork +# repositories. +# +# The doc stage, and the deploy-doc job, is executed on all branches of the +# upstream repository. +# +# The build stage, and the remaining jobs in the deploy stage, are only +# executed on the upstream repository, and only for release tags. +# +# The deploy stages are manually instantiated. +########################################################################### + + +stages: + - test + - doc + - build + - deploy + + +############################################################################### +# A number of variables must be set for the jobs to work. The following +# variables are implicitly defined in any gitlab CI job: +# +# - CI_PROJECT_PATH - gitlab namespace/project +# - CI_COMMIT_REF_NAME - branch name, provided by gitlab +# - CI_COMMIT_TAG - present if build is running on a tag +# +# These variables must be explicitly set as "secret" variables: +# +# - SSH_PRIVATE_KEY_GIT - private key for git login to remote host +# (UPSTREAM_URL) +# +# - SSH_PRIVATE_KEY_FSL_DOWNLOAD - private key for downloading some FSL +# files from a remote server (FSL_HOST) +# +# - SSH_PRIVATE_KEY_DOC_DEPLOY - private key for rsyncing documentation +# to remote host (DOC_HOST) +# +# - SSH_SERVER_HOSTKEYS - List of trusted SSH hosts +# +# - TWINE_PASSWORD: - Password to use when uploading to pypi +############################################################################### + + +variables: + UPSTREAM_PROJECT: "fsl/fslpy" + UPSTREAM_URL: "git@git.fmrib.ox.ac.uk" + DOC_HOST: "paulmc@jalapeno.fmrib.ox.ac.uk" + FSL_HOST: "paulmc@jalapeno.fmrib.ox.ac.uk" + TWINE_USERNAME: "pauldmccarthy" + TWINE_REPOSITORY_URL: "https://testpypi.python.org/pypi" + + +#################################### +# These anchors are used to restrict +# when and where jobs are executed. +#################################### + + +.only_upstream: &only_upstream + only: + - branches@fsl/fslpy + + +.only_master: &only_master + only: + - master@fsl/fslpy + + +.only_releases: &only_releases + only: + - tags@fsl/fslpy + + +.except_releases: &except_releases + except: + - tags + + +########################################################## +# The setup_ssh anchor contains a before_script section +# which does the following: +# +# - Sets up key-based SSH login, and +# installs the private keys, so +# we can connect to servers. +# +# - Configures git, and adds the +# upstream repo as a remote +# +# (see https://docs.gitlab.com/ce/ci/ssh_keys/README.html) +# +# NOTE: It is assumed that non-docker +# executors are already configured +# (or don't need any configuration). +########################################################## + + +.setup_ssh: &setup_ssh + before_script: + + - if [[ -f /.dockerenv ]]; then + + apt-get update -y || yum -y check-update || true; + apt-get install -y openssh-client || yum install -y openssh-client || true; + apt-get install -y rsync || yum install -y rsync || true; + + eval $(ssh-agent -s); + mkdir -p $HOME/.ssh; + + echo "$SSH_PRIVATE_KEY_GIT" > $HOME/.ssh/id_git; + echo "$SSH_PRIVATE_KEY_FSL_DOWNLOAD" > $HOME/.ssh/id_fsl_download; + + if [[ "$CI_PROJECT_PATH" == "$UPSTREAM_PROJECT" ]]; then + echo "$SSH_PRIVATE_KEY_DOC_DEPLOY" > $HOME/.ssh/id_doc_deploy; + fi; + + chmod go-rwx $HOME/.ssh/id_*; + + ssh-add $HOME/.ssh/id_git; + ssh-add $HOME/.ssh/id_fsl_download; + + if [[ "$CI_PROJECT_PATH" == "$UPSTREAM_PROJECT" ]]; then + ssh-add $HOME/.ssh/id_doc_deploy; + fi + + echo "$SSH_SERVER_HOSTKEYS" > $HOME/.ssh/known_hosts; + + touch $HOME/.ssh/config; + + echo "Host ${UPSTREAM_URL##*@}" >> $HOME/.ssh/config; + echo " User ${UPSTREAM_URL%@*}" >> $HOME/.ssh/config; + echo " IdentityFile $HOME/.ssh/id_git" >> $HOME/.ssh/config; + + echo "Host docdeploy" >> $HOME/.ssh/config; + echo " HostName ${DOC_HOST##*@}" >> $HOME/.ssh/config; + echo " User ${DOC_HOST%@*}" >> $HOME/.ssh/config; + echo " IdentityFile $HOME/.ssh/id_doc_deploy" >> $HOME/.ssh/config; + + echo "Host fsldownload" >> $HOME/.ssh/config; + echo " HostName ${FSL_HOST##*@}" >> $HOME/.ssh/config; + echo " User ${FSL_HOST%@*}" >> $HOME/.ssh/config; + echo " IdentityFile $HOME/.ssh/id_fsl_download" >> $HOME/.ssh/config; + + echo "Host *" >> $HOME/.ssh/config; + echo " IdentitiesOnly yes" >> $HOME/.ssh/config; + + git config --global user.name "Gitlab CI"; + git config --global user.email "gitlabci@localhost"; + + if [[ `git remote -v` == *"upstream"* ]]; then + git remote remove upstream; + fi; + git remote add upstream "$UPSTREAM_URL:$UPSTREAM_PROJECT"; + fi + + +################################################### +# The patch_version anchor contains a before_script +# section which is run on release builds, and makes +# sure that the version in the code is up to date +# (i.e. equal to the tag name). +################################################### + + +.patch_version: &patch_version + before_script: + + - if [[ "x$CI_COMMIT_TAG" != "x" ]]; then + echo "Release detected - patching version - $CI_COMMIT_REF_NAME"; + python -c "import fsl.version as v; v.patchVersion('fsl/version.py', '$CI_COMMIT_REF_NAME')"; + fi + + +############ +# Test stage +############ + + +.test: &test_template + stage: test + <<: *setup_ssh + + # Releases are just tags on a release + # branch, so we don't need to test them. + <<: *except_releases + + tags: + - docker + script: - - cat requirements.txt | xargs -n 1 pip install + + # If running on a fork repository, we merge in the + # upstream/master branch. This is done so that merge + # requests from fork to the parent repository will + # have unit tests run on the merged code, something + # which gitlab CE does not currently do for us. + - if [[ "$CI_PROJECT_PATH" != "$UPSTREAM_PROJECT" ]]; then + git fetch upstream; + git merge --no-commit --no-ff upstream/master; + fi; + + # I am currently assuming that we are + # running a debian 8/jessie container + # (the python:2.7 and 3.6 images are + # based on this). + # We need to install xvfb, and all of + # the wxpython dependencies. + - apt-get update -y + - apt-get install -y xvfb libgtk-3-0 + - apt-get install -y libnotify4 freeglut3 libsdl1.2debian + + # Linux builds for wxPython are currently not + # on pypi, but are available at this url. + - pip install -f https://wxpython.org/Phoenix/release-extras/linux/gtk3/debian-8/ wxpython + + # All other deps can be installed as + # normal. scipy is required by nibabel, + # but not listed in its requirements. + # We install test dependencies through + # pip, because if we let setuptools do + # it, it will build/install everything + # from source, rather than using wheels. + - pip install -r requirements.txt - pip install scipy - - pip install coverage - - su -s /bin/bash -c "xvfb-run python setup.py test" nobody - - coverage report -m + - pip install sphinx sphinx-rtd-theme + - pip install pytest pytest-cov pytest-html pytest-runner mock coverage + + # We need the FSL atlases for the atlas + # tests, and need $FSLDIR to be defined + - export FSLDIR=/fsl/ + - mkdir -p $FSLDIR/data/ + - rsync -rv "fsldownload:data/atlases/" "$FSLDIR/data/atlases/" + + # Finally, run the damned tests. + + # We run some tests under xvfb-run + # because they invoke wx. Sleep in + # between, otherwise xvfb gets upset. + - xvfb-run python setup.py test --addopts="tests/test_async.py" + - sleep 5 + - xvfb-run python setup.py test --addopts="tests/test_platform.py" + + # We run the immv/imcpy tests as the nobody + # user because some tests expect permission + # denied errors when looking at files, and + # root never gets denied. Make everything in + # this directory writable by anybody (which, + # unintuitively, includes nobody) + - chmod -R a+w `pwd` + - su -s /bin/bash -c "python setup.py test --addopts='tests/test_immv_imcp.py'" nobody + + # All other tests can be run as normal + - python setup.py test --addopts="--ignore=tests/test_async.py --ignore=tests/test_platform.py --ignore=tests/test_immv_imcp.py" + - python -m coverage report + + +test:2.7: + <<: *test_template + image: python:2.7 + + +test:3.4: + <<: *test_template + image: python:3.4 + + +# a wxphoenix/3.5 build +# is not yet available +# test:3.5: +# <<: *test_template +# image: python:3.5 + + +# a wxphoenix/3.6 build +# is not yet available +# test:3.6: +# <<: *test_template +# image: python:3.6 + + +########### +# Doc stage +########### + +build-doc: + <<: *only_upstream + <<: *patch_version + + tags: + - docker + + stage: doc + image: python:3.5 -test:3.6: - image: fsleyes-py36 script: - - cat requirements.txt | xargs -n 1 pip install - - pip install scipy - - pip install coverage - - su -s /bin/bash -c "xvfb-run python setup.py test" nobody - - coverage report -m + - python setup.py doc + - mv doc/html doc/"$CI_COMMIT_REF_NAME" + artifacts: + paths: + - doc/$CI_COMMIT_REF_NAME + + +############# +# Build stage +############# + + +build-dist: + <<: *only_releases + <<: *patch_version + + stage: build + image: python:3.5 + + script: + - pip install wheel + - python setup.py sdist + - python setup.py bdist_wheel + + artifacts: + paths: + - dist/* + + +############## +# Deploy stage +############## + + +deploy-doc: + <<: *only_upstream + <<: *setup_ssh + stage: deploy + when: manual + image: python:3.5 + + tags: + - docker + + dependencies: + - build-doc + + script: + - rsync -rv doc/"$CI_COMMIT_REF_NAME" "docdeploy:" + + +deploy-pypi: + <<: *only_releases + <<: *setup_ssh + stage: deploy + when: manual + image: python:3.5 + + tags: + - docker + + dependencies: + - build-dist + + script: + - pip install setuptools wheel twine + - twine upload dist/* diff --git a/MANIFEST.in b/MANIFEST.in index 43a8f2413ac698be518bddfce023c8de834c71bc..bbc7ab460f7f8b4ab9b1b2d0ba261621b3825502 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,5 @@ include LICENSE include COPYRIGHT -include README.md include requirements.txt include pytest.ini recursive-include doc * diff --git a/README.md b/README.md deleted file mode 100644 index b79542f1663cb2d807c408f8f6630b40e72e277e..0000000000000000000000000000000000000000 --- a/README.md +++ /dev/null @@ -1,42 +0,0 @@ -fslpy -===== - - -The `fslpy` project is a [FSL](http://fsl.fmrib.ox.ac.uk/fsl/fslwiki/) -programming library written in Python. It is used by -[FSLeyes](https://git.fmrib.ox.ac.uk/paulmc/fsleyes/). - - -Dependencies ------------- - - -All of the dependencies of `fslpy` are listed in the -[requirements.txt](requirements.txt) file. Some `fslpy` modules require -[wxPython](http://www.wxpython.org) 3.0.2.0. - - -Documentation -------------- - -`fslpy` is documented using [sphinx](http://http://sphinx-doc.org/). You can -build the API documentation by running: - - python setup.py doc - -The HTML documentation will be generated and saved in the `doc/html/` directory. - - -If you are interested in contributing to `fslpy`, check out the [contributing -guide](doc/contributing.rst). - - -Tests ------ - -Run the test suite via: - - python setup.py test - -A test report will be generated at `report.html`, and a code coverage report -will be generated in `htmlcov/`. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000000000000000000000000000000000000..a23c2efa038d259755064d992a570e03cc19e58b --- /dev/null +++ b/README.rst @@ -0,0 +1,50 @@ +fslpy +===== + + +.. image:: https://git.fmrib.ox.ac.uk/fsl/fslpy/badges/master/build.svg + :target: https://git.fmrib.ox.ac.uk/fsl/fslpy/commits/master/ + +.. image:: https://git.fmrib.ox.ac.uk/fsl/fslpy/badges/master/coverage.svg + :target: https://git.fmrib.ox.ac.uk/fsl/fslpy/commits/master/ + + +The ``fslpy`` project is a `FSL <http://fsl.fmrib.ox.ac.uk/fsl/fslwiki/>`_ +programming library written in Python. It is used by `FSLeyes +<https://git.fmrib.ox.ac.uk/paulmc/fsleyes/>`_. + + +Dependencies +------------ + + +All of the dependencies of ``fslpy`` are listed in the `requirements.txt +<requirements.txt>`_ file. Some ``fslpy`` modules require `wxPython +<http://www.wxpython.org>`_ 3.0.2.0 or higher. + + +Documentation +------------- + +``fslpy`` is documented using `sphinx <http://http://sphinx-doc.org/>`_. You +can build the API documentation by running:: + + python setup.py doc + +The HTML documentation will be generated and saved in the ``doc/html/`` +directory. + + +If you are interested in contributing to ``fslpy``, check out the +`contributing guide <doc/contributing.rst>`_. + + +Tests +----- + +Run the test suite via:: + + python setup.py test + +A test report will be generated at ``report.html``, and a code coverage report +will be generated in ``htmlcov/``. diff --git a/doc/contributing.rst b/doc/contributing.rst index e2b1194869df1aa1db5feb76c4a7be26527681d2..8712dc6b0eb1a893356a36653a9d83a9f75e0390 100644 --- a/doc/contributing.rst +++ b/doc/contributing.rst @@ -45,6 +45,12 @@ numbers:: backwards-incompatible changes. +The version number in the ``master`` branch should be of the form +``major.minor.patch.dev``, to indicate that any releases made from this branch +are development releases (although development releases are not part of the +release model). + + Releases -------- @@ -72,12 +78,10 @@ Testing Unit and integration tests are currently run with ``py.test`` and -``coverage``. We don't have CI configured yet, so tests have to be run -manually. +``coverage``. - Aim for 100% code coverage. -- Tests must pass on both python 2.7 and 3.5 -- Tests must pass on both wxPython 3.0.2.0 and 4.0.0 +- Tests must pass on python 2.7, 3.4, 3.5, and 3.6 Coding conventions diff --git a/fsl/utils/memoize.py b/fsl/utils/memoize.py index 1a95638c87ca3c862c84307ef9b9b8a3fbb9e62b..8f56a1a61eb9fa3346da69a27d3da13fdccc0b5d 100644 --- a/fsl/utils/memoize.py +++ b/fsl/utils/memoize.py @@ -55,14 +55,14 @@ def memoize(func): try: result = cache[key] - log.debug('Retrieved from cache[{}]: {}'.format(key, result)) + log.debug(u'Retrieved from cache[{}]: {}'.format(key, result)) except KeyError: result = func(*a, **kwa) cache[key] = result - log.debug('Adding to cache[{}]: {}'.format(key, result)) + log.debug(u'Adding to cache[{}]: {}'.format(key, result)) return result return wrapper @@ -82,8 +82,15 @@ def memoizeMD5(func): hashobj = hashlib.md5() + # Convert each arg to a string + # representation, then encode + # it into a sequence of (utf-8 + # compatible) bytes , and take + # the hash of those bytes. for arg in args: - arg = six.u(str(arg)).encode('utf-8') + if not isinstance(arg, six.string_types): + arg = str(arg) + arg = arg.encode('utf-8') hashobj.update(arg) digest = hashobj.hexdigest() @@ -94,7 +101,7 @@ def memoizeMD5(func): result = func(*args, **kwargs) - log.debug('Adding to MD5 cache[{}]: {}'.format( + log.debug(u'Adding to MD5 cache[{}]: {}'.format( digest, result)) cache[digest] = result diff --git a/fsl/version.py b/fsl/version.py index 9acc9d7fd9b8247e8cf26252b5e34c05df77064f..68a81a1e9b28ccc8f8b2f9072ed28dad62f06217 100644 --- a/fsl/version.py +++ b/fsl/version.py @@ -5,7 +5,7 @@ # Author: Paul McCarthy <pauldmccarthy@gmail.com> # """The primary purpose of this module is as a container for the ``fslpy`` -version number. A couple of conveniense functions for comparing version +version number. A handful of convenience functions for managing version numbers are also defined here. .. autosummary:: @@ -14,10 +14,13 @@ numbers are also defined here. __version__ parseVersionString compareVersions + patchVersion The ``fslpy`` version number consists of three numbers, separated by a period, -which roughly obeys the Semantic Versioning conventions (http://semver.org/): +roughly obeys the Semantic Versioning conventions (http://semver.org/), and +is compatible with PEP 440 (https://www.python.org/dev/peps/pep-0440/): + 1. The major release number. This gets updated for major/external releases. @@ -26,13 +29,19 @@ which roughly obeys the Semantic Versioning conventions (http://semver.org/): 3. The point release number. This gets updated for minor/internal releases, which primarily involve bug-fixes and minor changes. + + +The sole exception to the above convention are evelopment versions, which end +in ``'.dev'``. """ -import string +import os.path as op +import re +import string -__version__ = 'dev' +__version__ = '1.0.2.dev' """Current version number, as a string. """ @@ -44,21 +53,26 @@ def parseVersionString(versionString): An error is raised if the ``versionString`` is invalid. """ - if versionString == 'dev': - return 9999, 9999, 9999 - components = versionString.split('.') + # Truncate after three elements - + # a development (unreleased0 version + # number will end with '.dev', but + # we ignore this for the purposes of + # comparison. + if len(components) == 4 and components[3] == 'dev': + components = components[:3] + # Major, minor, and point # version are always numeric major, minor, point = [c for c in components] - # Early versions of FSLeyes + # But early versions of FSLeyes # used a letter at the end # to denote a hotfix release. # Don't break if we get one # of these old version numbers. - point = point.strip(string.ascii_letters) + point = point.rstrip(string.ascii_letters) return [int(c) for c in [major, minor, point]] @@ -91,3 +105,27 @@ def compareVersions(v1, v2, ignorePoint=False): if p1 < p2: return -1 return 0 + + +def patchVersion(filename, newversion): + """Patches the given ``filename``, in place, with the given + ``newversion``. Searches for a line of the form:: + + __version__ = '<oldversion>' + + and replaces ``<oldversion>`` with ``newversion``. + """ + filename = op.abspath(filename) + + with open(filename, 'rt') as f: + lines = f.readlines() + + pattern = re.compile('^__version__ *= *\'.*\' *$') + + for i, line in enumerate(lines): + if pattern.match(line): + lines[i] = '__version__ = \'{0}\'\n'.format(newversion) + break + + with open(filename, 'wt') as f: + lines = f.writelines(lines) diff --git a/pytest.ini b/pytest.ini index 4352354c39baad6c64cc31779521515065481c51..374b89e670b74d8e492393ec619249b1c5ee9cf2 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,3 @@ [pytest] testpaths = tests -addopts = -s -v --niters=50 --cov=fsl --cov-report=html --html=report.html +addopts = -s -v --niters=50 --cov=fsl --cov-report=html --html=report.html --cov-append diff --git a/requirements.txt b/requirements.txt index dd852d16ed620cb5d8943efe4fb353c716e49865..55d5ba9466e17d361649fd89f284c5c7c1337986 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -six>=1.10.0,<2.0 -numpy>=1.11.1,<2.0 -nibabel>=2.1,<3.0 -indexed_gzip>=0.3.3,<0.4 -wxPython>=3.0.2.0,<=4.0.0a2 +six==1.* +numpy==1.* +nibabel==2.* +indexed_gzip==0.3.* +wxPython>=3.0.2.0,<4.1 diff --git a/setup.py b/setup.py index 8cb8e1dff62868e3fb34cfdd161c30c6b4db3be3..37848d4f041e74f671c6396cf67e2d8732192b77 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ with open(op.join(basedir, "fsl", "version.py")) as f: break version = version['__version__'] -with open(op.join(basedir, 'README.md'), 'rt') as f: +with open(op.join(basedir, 'README.rst'), 'rt') as f: readme = f.read() @@ -98,7 +98,9 @@ setup( 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Topic :: Software Development :: Libraries :: Python Modules'], packages=packages, @@ -106,6 +108,7 @@ setup( setup_requires=['pytest-runner', 'sphinx', 'sphinx-rtd-theme', 'mock'], tests_require=['mock', + 'coverage', 'pytest-cov', 'pytest-html', 'pytest-runner', diff --git a/tests/test_memoize.py b/tests/test_memoize.py index 4e7142328b287a67eea223b05cd45bcb22bb0e4e..63ce1a267ba5ee00e3551e7ea81bf27b3c618054 100644 --- a/tests/test_memoize.py +++ b/tests/test_memoize.py @@ -5,6 +5,7 @@ # Author: Paul McCarthy <pauldmccarthy@gmail.com> # +import six import numpy as np @@ -17,14 +18,13 @@ def test_memoize(): def thefunc(*args, **kwargs): timesCalled[0] += 1 - + if len(args) + len(kwargs) == 0: return 0 elif len(args) == 1: return args[0] * 5 else: return kwargs['value'] * 5 memoized = memoize.memoize(thefunc) - # No args for i in range(5): assert memoized() == 0 @@ -42,6 +42,13 @@ def test_memoize(): assert memoized(value=i) == i * 5 assert timesCalled[0] == 6 + # Unicode arg + s = six.u('\u25B2') + assert memoized(s) == s * 5 + assert timesCalled[0] == 7 + assert memoized(s) == s * 5 + assert timesCalled[0] == 7 + def test_memoizeMD5(): timesCalled = [0] @@ -50,7 +57,7 @@ def test_memoizeMD5(): timesCalled[0] += 1 if len(args) + len(kwargs) == 0: return 0 elif len(args) == 1: return args[0] * 5 - else: return kwargs['value'] * 5 + else: return kwargs['value'] * 5 memoized = memoize.memoizeMD5(thefunc) @@ -69,7 +76,14 @@ def test_memoizeMD5(): for i in range(3): for i in range(5): assert memoized(value=i) == i * 5 - assert timesCalled[0] == 6 + assert timesCalled[0] == 6 + + # Unicode arg (and return value) + s = six.u('\u25B2') + assert memoized(s) == s * 5 + assert timesCalled[0] == 7 + assert memoized(s) == s * 5 + assert timesCalled[0] == 7 def test_skipUnchanged(): @@ -90,66 +104,66 @@ def test_skipUnchanged(): wrapped('key1', 11) wrapped('key2', 12) wrapped('key3', 13) - + assert timesCalled['key1'] == 1 assert timesCalled['key2'] == 1 assert timesCalled['key3'] == 1 - + wrapped('key1', 11) wrapped('key2', 12) wrapped('key3', 13) - + assert timesCalled['key1'] == 1 assert timesCalled['key2'] == 1 - assert timesCalled['key3'] == 1 + assert timesCalled['key3'] == 1 wrapped('key1', 14) wrapped('key2', 15) wrapped('key3', 16) - + assert timesCalled['key1'] == 2 assert timesCalled['key2'] == 2 - assert timesCalled['key3'] == 2 - + assert timesCalled['key3'] == 2 + wrapped('key1', 14) wrapped('key2', 15) wrapped('key3', 16) - + assert timesCalled['key1'] == 2 assert timesCalled['key2'] == 2 assert timesCalled['key3'] == 2 - + wrapped('key1', 11) wrapped('key2', 12) wrapped('key3', 13) - + assert timesCalled['key1'] == 3 assert timesCalled['key2'] == 3 - assert timesCalled['key3'] == 3 - + assert timesCalled['key3'] == 3 + wrapped('key1', np.array([11, 12])) wrapped('key2', np.array([13, 14])) wrapped('key3', np.array([15, 16])) - + assert timesCalled['key1'] == 4 assert timesCalled['key2'] == 4 - assert timesCalled['key3'] == 4 + assert timesCalled['key3'] == 4 wrapped('key1', np.array([12, 11])) wrapped('key2', np.array([14, 13])) wrapped('key3', np.array([16, 15])) - + assert timesCalled['key1'] == 5 assert timesCalled['key2'] == 5 - assert timesCalled['key3'] == 5 + assert timesCalled['key3'] == 5 wrapped('key1', np.array([12, 11])) wrapped('key2', np.array([14, 13])) wrapped('key3', np.array([16, 15])) - + assert timesCalled['key1'] == 5 assert timesCalled['key2'] == 5 - assert timesCalled['key3'] == 5 + assert timesCalled['key3'] == 5 def test_Instanceify(): @@ -169,7 +183,7 @@ def test_Instanceify(): @memoize.Instanceify(memoize.skipUnchanged) def setter2(self, name, value): self.setter2Called += 1 - + @memoize.Instanceify(memoize.memoize) def func1(self, arg): self.func1Called += 1 @@ -196,41 +210,41 @@ def test_Instanceify(): c1.setter1('blob', 120) c1.check(1, 0, 0, 0) c2.check(0, 0, 0, 0) - + for i in range(3): c1.setter1('blob', 150) c1.check(2, 0, 0, 0) - c2.check(0, 0, 0, 0) - + c2.check(0, 0, 0, 0) + for i in range(3): c1.setter1('flob', 200) c1.check(3, 0, 0, 0) c2.check(0, 0, 0, 0) - + for i in range(3): c1.setter1('flob', 180) c1.check(4, 0, 0, 0) c2.check(0, 0, 0, 0) - + for i in range(3): c2.setter1('blob', 120) c1.check(4, 0, 0, 0) c2.check(1, 0, 0, 0) - + for i in range(3): c2.setter1('blob', 150) c1.check(4, 0, 0, 0) - c2.check(2, 0, 0, 0) - + c2.check(2, 0, 0, 0) + for i in range(3): c2.setter1('flob', 200) c1.check(4, 0, 0, 0) c2.check(3, 0, 0, 0) - + for i in range(3): c2.setter1('flob', 180) c1.check(4, 0, 0, 0) - c2.check(4, 0, 0, 0) + c2.check(4, 0, 0, 0) # Call setter2 on one instance, # ... @@ -238,63 +252,63 @@ def test_Instanceify(): c1.setter2('blob', 120) c1.check(4, 1, 0, 0) c2.check(4, 0, 0, 0) - + for i in range(3): c1.setter2('blob', 150) c1.check(4, 2, 0, 0) - c2.check(4, 0, 0, 0) - + c2.check(4, 0, 0, 0) + for i in range(3): c1.setter2('flob', 200) c1.check(4, 3, 0, 0) c2.check(4, 0, 0, 0) - + for i in range(3): c1.setter2('flob', 180) c1.check(4, 4, 0, 0) c2.check(4, 0, 0, 0) - + for i in range(3): c2.setter2('blob', 120) c1.check(4, 4, 0, 0) c2.check(4, 1, 0, 0) - + for i in range(3): c2.setter2('blob', 150) c1.check(4, 4, 0, 0) - c2.check(4, 2, 0, 0) - + c2.check(4, 2, 0, 0) + for i in range(3): c2.setter2('flob', 200) c1.check(4, 4, 0, 0) c2.check(4, 3, 0, 0) - + for i in range(3): c2.setter2('flob', 180) c1.check(4, 4, 0, 0) - c2.check(4, 4, 0, 0) - + c2.check(4, 4, 0, 0) + # Call func1 on one instance, # ... for i in range(3): assert c1.func1(123) == 246 c1.check(4, 4, 1, 0) c2.check(4, 4, 0, 0) - + for i in range(3): assert c1.func1(456) == 912 c1.check(4, 4, 2, 0) - c2.check(4, 4, 0, 0) - + c2.check(4, 4, 0, 0) + for i in range(3): assert c2.func1(123) == 246 c1.check(4, 4, 2, 0) c2.check(4, 4, 1, 0) - + for i in range(3): assert c2.func1(456) == 912 c1.check(4, 4, 2, 0) - c2.check(4, 4, 2, 0) + c2.check(4, 4, 2, 0) # Call func2 on one instance, # ... @@ -302,16 +316,16 @@ def test_Instanceify(): assert c1.func2(123) == 492 c1.check(4, 4, 2, 1) c2.check(4, 4, 2, 0) - + for i in range(3): assert c1.func2(456) == 1824 - + for i in range(3): assert c2.func2(123) == 492 c1.check(4, 4, 2, 2) c2.check(4, 4, 2, 1) - + for i in range(3): assert c2.func2(456) == 1824 c1.check(4, 4, 2, 2) - c2.check(4, 4, 2, 2) + c2.check(4, 4, 2, 2)