├── tests ├── conftest.py └── test_freezegun.py ├── .coveragerc ├── MANIFEST.in ├── tox.ini ├── CHANGELOG.md ├── .travis.yml ├── LICENSE ├── appveyor.yml ├── .gitignore ├── pytest_freezegun.py ├── setup.py └── README.rst /tests/conftest.py: -------------------------------------------------------------------------------- 1 | pytest_plugins = 'pytester' 2 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [paths] 2 | source = pytest_freezegun 3 | 4 | [run] 5 | branch = true 6 | source = pytest_freezegun 7 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | 4 | recursive-exclude * __pycache__ 5 | recursive-exclude * *.py[co] 6 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # For more information about tox, see https://tox.readthedocs.io/en/latest/ 2 | [tox] 3 | envlist = {py27,py34,py35,py36,pypy}-{pt3,pt4},flake8 4 | 5 | [testenv] 6 | deps = 7 | coverage 8 | pt3: pytest>=3,<4 9 | pt4: pytest>=4,<5 10 | commands = coverage run -p -m pytest tests/ {posargs} 11 | 12 | [testenv:flake8] 13 | skip_install = true 14 | deps = flake8 15 | commands = flake8 pytest_freezegun.py setup.py tests 16 | 17 | [pytest] 18 | filterwarnings = 19 | once::DeprecationWarning 20 | once::PendingDeprecationWarning 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [0.3.0] - 2018-11-15 8 | ### Added 9 | - Support for pytest 3.6+, including 4.0. Thanks to [Oliver Sauder](https://github.com/sliverc) and [Nate Parsons](https://github.com/nsp). 10 | 11 | ### Removed 12 | - Support for freezegun 0.3.0 (it might still work, but is not tested anymore) 13 | - Support for Python 3.3 (it might still work, but is not tested anymore) 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | 3 | sudo: false 4 | language: python 5 | python: 6 | - "2.7" 7 | - "3.4" 8 | - "3.5" 9 | - "3.6" 10 | - "pypy" 11 | env: 12 | - TOX_SUFFIX=pt3 13 | - TOX_SUFFIX=pt4 14 | 15 | install: 16 | # coveralls-python needs a newer setuptools to install from git 17 | - "[[ $TRAVIS_PYTHON_VERSION == pypy ]] || pip install --upgrade setuptools pip" 18 | # Using the coveralls-python master until parallel builds support is released 19 | # See: https://github.com/coveralls-clients/coveralls-python/commit/7ba3a5 20 | - "[[ $TRAVIS_PYTHON_VERSION == pypy ]] || pip install git+https://github.com/coveralls-clients/coveralls-python" 21 | - pip install tox coverage 22 | - "TOX_ENV=${TRAVIS_PYTHON_VERSION/[0-9].[0-9]/py${TRAVIS_PYTHON_VERSION/.}}-$TOX_SUFFIX" 23 | script: 24 | - tox -e $TOX_ENV 25 | - coverage combine 26 | after_success: 27 | - "[[ $TRAVIS_PYTHON_VERSION == pypy ]] || COVERALLS_PARALLEL=true coveralls" 28 | 29 | before_cache: 30 | - rm -rf $HOME/.cache/pip/log 31 | cache: 32 | directories: 33 | - $HOME/.cache/pip 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2017 Tomasz Kontusz 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # What Python version is installed where: 2 | # http://www.appveyor.com/docs/installed-software#python 3 | 4 | environment: 5 | matrix: 6 | - PYTHON: "C:\\Python27" 7 | TOX_ENV: "py27-pt3" 8 | 9 | - PYTHON: "C:\\Python27" 10 | TOX_ENV: "py27-pt4" 11 | 12 | - PYTHON: "C:\\Python34" 13 | TOX_ENV: "py34-pt3" 14 | 15 | - PYTHON: "C:\\Python35" 16 | TOX_ENV: "py35-pt4" 17 | 18 | - PYTHON: "C:\\Python36" 19 | TOX_ENV: "py36-pt3" 20 | 21 | - PYTHON: "C:\\Python36" 22 | TOX_ENV: "py36-pt4" 23 | 24 | init: 25 | - "%PYTHON%/python -V" 26 | - "%PYTHON%/python -c \"import struct;print( 8 * struct.calcsize(\'P\'))\"" 27 | 28 | install: 29 | - "%PYTHON%/Scripts/easy_install -U pip" 30 | - "%PYTHON%/Scripts/pip install tox" 31 | - "%PYTHON%/Scripts/pip install wheel" 32 | 33 | build: false # Not a C# project, build stuff at the test step instead. 34 | 35 | test_script: 36 | - "%PYTHON%/Scripts/tox -e %TOX_ENV%" 37 | 38 | after_test: 39 | - "%PYTHON%/python setup.py bdist_wheel" 40 | - ps: "ls dist" 41 | 42 | artifacts: 43 | - path: dist\* 44 | 45 | #on_success: 46 | # - TODO: upload the content of dist/*.whl to a public wheelhouse 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | .pytest_cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask instance folder 58 | instance/ 59 | 60 | # Sphinx documentation 61 | docs/_build/ 62 | 63 | # MkDocs documentation 64 | /site/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | -------------------------------------------------------------------------------- /pytest_freezegun.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pytest 4 | 5 | from freezegun import freeze_time 6 | 7 | 8 | class FreezegunPlugin(object): 9 | def __init__(self): 10 | self.freezer = None 11 | self.frozen_time = None 12 | 13 | @pytest.fixture(name='freezer') 14 | def freezer_fixture(self): 15 | if self.frozen_time is not None: 16 | yield self.frozen_time 17 | else: 18 | with freeze_time() as frozen_time: 19 | yield frozen_time 20 | 21 | @pytest.hookimpl(tryfirst=True) 22 | def pytest_runtest_setup(self, item): 23 | try: 24 | marker = item.get_closest_marker('freeze_time') 25 | except AttributeError: # for pytest < 3.6.0 26 | marker = item.get_marker('freeze_time') 27 | 28 | if marker: 29 | ignore = marker.kwargs.pop('ignore', []) 30 | ignore.append('_pytest') 31 | 32 | self.freezer = freeze_time( 33 | *marker.args, 34 | ignore=ignore, 35 | **marker.kwargs 36 | ) 37 | self.frozen_time = self.freezer.start() 38 | 39 | @pytest.hookimpl(trylast=True) 40 | def pytest_runtest_teardown(self): 41 | if self.freezer is not None: 42 | self.freezer.stop() 43 | self.freezer = None 44 | self.frozen_time = None 45 | 46 | 47 | plugin = FreezegunPlugin() 48 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import codecs 6 | from setuptools import setup 7 | 8 | 9 | def read(fname): 10 | file_path = os.path.join(os.path.dirname(__file__), fname) 11 | return codecs.open(file_path, encoding='utf-8').read() 12 | 13 | 14 | setup( 15 | name='pytest-freezegun', 16 | version='0.3.0', 17 | author='Tomasz Kontusz', 18 | author_email='tomasz.kontusz@gmail.com', 19 | maintainer='Tomasz Kontusz', 20 | maintainer_email='tomasz.kontusz@gmail.com', 21 | license='MIT', 22 | url='https://github.com/ktosiek/pytest-freezegun', 23 | description='Wrap tests with fixtures in freeze_time', 24 | long_description=read('README.rst'), 25 | py_modules=['pytest_freezegun'], 26 | install_requires=[ 27 | 'freezegun>0.3', 28 | 'pytest>=3.0.0', 29 | ], 30 | classifiers=[ 31 | 'Development Status :: 4 - Beta', 32 | 'Framework :: Pytest', 33 | 'Intended Audience :: Developers', 34 | 'Topic :: Software Development :: Testing', 35 | 'Programming Language :: Python', 36 | 'Programming Language :: Python :: 2', 37 | 'Programming Language :: Python :: 2.7', 38 | 'Programming Language :: Python :: 3', 39 | 'Programming Language :: Python :: 3.4', 40 | 'Programming Language :: Python :: 3.5', 41 | 'Programming Language :: Python :: 3.6', 42 | 'Programming Language :: Python :: Implementation :: CPython', 43 | 'Programming Language :: Python :: Implementation :: PyPy', 44 | 'Operating System :: OS Independent', 45 | 'License :: OSI Approved :: MIT License', 46 | ], 47 | entry_points={ 48 | 'pytest11': [ 49 | 'freezegun = pytest_freezegun:plugin', 50 | ], 51 | }, 52 | ) 53 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | pytest-freezegun 3 | ================ 4 | 5 | .. image:: https://travis-ci.org/ktosiek/pytest-freezegun.svg?branch=master 6 | :target: https://travis-ci.org/ktosiek/pytest-freezegun 7 | :alt: See Build Status on Travis CI 8 | 9 | .. image:: https://ci.appveyor.com/api/projects/status/github/ktosiek/pytest-freezegun?branch=master&svg=true 10 | :target: https://ci.appveyor.com/project/ktosiek/pytest-freezegun/branch/master 11 | :alt: See Build Status on AppVeyor 12 | 13 | Wrap tests with fixtures in freeze_time 14 | 15 | 16 | Features 17 | -------- 18 | 19 | * Freeze time in both the test and fixtures 20 | * Access the freezer when you need it 21 | 22 | 23 | Installation 24 | ------------ 25 | 26 | You can install "pytest-freezegun" via `pip`_ from `PyPI`_:: 27 | 28 | $ pip install pytest-freezegun 29 | 30 | 31 | Usage 32 | ----- 33 | 34 | All the features can be seen in this example: 35 | 36 | .. code-block:: python 37 | 38 | @pytest.fixture 39 | def current_date(): 40 | return datetime.now().date() 41 | 42 | @pytest.mark.freeze_time('2017-05-21') 43 | def test_current_date(current_date): 44 | assert current_date == date(2017, 5, 21) 45 | 46 | @pytest.mark.freeze_time 47 | def test_changing_date(current_date, freezer): 48 | freezer.move_to('2017-05-20') 49 | assert current_date == date(2017, 5, 20) 50 | freezer.move_to('2017-05-21') 51 | assert current_date == date(2017, 5, 21) 52 | 53 | def test_not_using_marker(freezer): 54 | now = datetime.now() 55 | time.sleep(1) 56 | later = datetime.now() 57 | assert now == later 58 | 59 | Contributing 60 | ------------ 61 | Contributions are very welcome. 62 | Tests can be run with `tox`_. 63 | You can later check coverage with `coverage combine && coverage html`. 64 | Please try to keep coverage at least the same before you submit a pull request. 65 | 66 | License 67 | ------- 68 | 69 | Distributed under the terms of the `MIT`_ license, "pytest-freezegun" is free and open source software 70 | 71 | 72 | Issues 73 | ------ 74 | 75 | If you encounter any problems, please `file an issue`_ along with a detailed description. 76 | 77 | Credits 78 | ------- 79 | 80 | This `Pytest`_ plugin was generated with `Cookiecutter`_ along with `@hackebrot`_'s `Cookiecutter-pytest-plugin`_ template. 81 | 82 | 83 | .. _`Cookiecutter`: https://github.com/audreyr/cookiecutter 84 | .. _`@hackebrot`: https://github.com/hackebrot 85 | .. _`MIT`: http://opensource.org/licenses/MIT 86 | .. _`cookiecutter-pytest-plugin`: https://github.com/pytest-dev/cookiecutter-pytest-plugin 87 | .. _`file an issue`: https://github.com/ktosiek/pytest-freezegun/issues 88 | .. _`pytest`: https://github.com/pytest-dev/pytest 89 | .. _`tox`: https://tox.readthedocs.io/en/latest/ 90 | .. _`pip`: https://pypi.python.org/pypi/pip/ 91 | .. _`PyPI`: https://pypi.python.org/pypi 92 | -------------------------------------------------------------------------------- /tests/test_freezegun.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | 4 | from datetime import datetime 5 | 6 | 7 | def test_freezing_time(testdir): 8 | testdir.makepyfile(""" 9 | import pytest 10 | from datetime import date, datetime 11 | 12 | @pytest.mark.freeze_time('2017-05-20 15:42') 13 | def test_sth(): 14 | assert datetime.now().date() == date(2017, 5, 20) 15 | """) 16 | 17 | result = testdir.runpytest('-v', '-s') 18 | assert result.ret == 0 19 | 20 | 21 | def test_freezing_time_in_fixture(testdir): 22 | testdir.makepyfile(""" 23 | import pytest 24 | from datetime import date, datetime 25 | 26 | @pytest.fixture 27 | def today(): 28 | return datetime.now().date() 29 | 30 | @pytest.mark.freeze_time('2017-05-20 15:42') 31 | def test_sth(today): 32 | assert today == date(2017, 5, 20) 33 | """) 34 | 35 | result = testdir.runpytest('-v', '-s') 36 | assert result.ret == 0 37 | 38 | 39 | def test_no_mark(testdir): 40 | testdir.makepyfile(""" 41 | import datetime 42 | 43 | def test_sth(): 44 | assert datetime.datetime.now() > {} 45 | """.format(repr(datetime.now()))) 46 | 47 | result = testdir.runpytest('-v', '-s') 48 | assert result.ret == 0 49 | 50 | 51 | def test_move_to(testdir): 52 | testdir.makepyfile(""" 53 | from datetime import date 54 | import pytest 55 | 56 | @pytest.mark.freeze_time 57 | def test_changing_date(freezer): 58 | freezer.move_to('2017-05-20') 59 | assert date.today() == date(2017, 5, 20) 60 | freezer.move_to('2017-05-21') 61 | assert date.today() == date(2017, 5, 21) 62 | """) 63 | 64 | result = testdir.runpytest('-v', '-s') 65 | assert result.ret == 0 66 | 67 | 68 | def test_durations(testdir): 69 | """ 70 | In an older version, the time would be frozen for the pytest itself too. 71 | That caused it to report weird durations for test runs, 72 | namely very large numbers for setup and very large 73 | NEGATIVE numbers for teardown. 74 | """ 75 | testdir.makepyfile(""" 76 | import pytest 77 | from datetime import date, datetime 78 | 79 | @pytest.mark.freeze_time('2000-01-01') 80 | def test_truth(): 81 | assert True 82 | """) 83 | 84 | result = testdir.runpytest('-vv', '-s', '--durations=3') 85 | 86 | # We don't have access to the actual terminalreporter, 87 | # so the only way to collect duration times is 88 | # to parse the pytest output. 89 | DURATION_REGEX = re.compile(r''' 90 | (-?\d+\.\d+)s # Time in seconds 91 | \s+ # Whitespace 92 | (call|setup|teardown) # Test phase 93 | \s+ # Whitespace 94 | test_durations.py::test_truth # Test ID 95 | ''', re.X) 96 | 97 | durations = {} 98 | for line in result.outlines: 99 | match = DURATION_REGEX.match(line) 100 | if match is None: 101 | continue 102 | 103 | durations[match.group(2)] = float(match.group(1)) 104 | 105 | # It should take a non-negative amount of time for each of the steps, 106 | # but it also should never take longer than a second 107 | assert 0 <= durations['setup'] <= 1 108 | assert 0 <= durations['call'] <= 1 109 | assert 0 <= durations['teardown'] <= 1 110 | 111 | 112 | def test_fixture_no_mark(testdir): 113 | testdir.makepyfile(""" 114 | from datetime import datetime 115 | import time 116 | 117 | def test_just_fixture(freezer): 118 | now = datetime.now() 119 | time.sleep(0.1) 120 | later = datetime.now() 121 | 122 | assert now == later 123 | """) 124 | 125 | result = testdir.runpytest('-v', '-s') 126 | assert result.ret == 0 127 | --------------------------------------------------------------------------------