Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@ Tests can be run with `tox`. Some basic code style tests can be run with `tox
-e flake8` and most other targets run the test suite with various supported
Python interpreters.

Tests use the real date by default. For reproducible builds, set
`STDNUM_TEST_DATE` to an explicit date in `YYYY-MM-DD` format, for example
`STDNUM_TEST_DATE=2026-09-21 tox -e py312`. The same variable works with a
direct `pytest` invocation. Invalid or empty values fail with a configuration
error.

This test-only option replaces `datetime.date.today()` during the test session;
it does not change the host clock or the installed library. Date construction
still returns ordinary date objects. Choose a reference date appropriate for
the release being tested. Leave the variable unset for development and
scheduled tests so that changes caused by the passage of time remain visible.

Module implementations have a couple of smaller test cases that also serve as
basic documentation of the happy flow.

Expand Down
72 changes: 72 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# conftest.py - optional reference date for tests
#
# Copyright (C) 2026 Ryan Duguid
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, see <https://www.gnu.org/licenses/>.

"""Pytest configuration for python-stdnum."""

import datetime
import os
from unittest.mock import patch

import pytest


pytest_plugins = ['pytester']


def pytest_configure(config):
"""Validate the optional reference date before collecting tests."""
value = os.environ.get('STDNUM_TEST_DATE')
config._stdnum_reference_date = None
if value is None:
return
try:
fixed_date = datetime.date.fromisoformat(value)
if fixed_date.isoformat() != value:
raise ValueError()
except ValueError:
raise pytest.UsageError('STDNUM_TEST_DATE must be a valid date in YYYY-MM-DD format')
config._stdnum_reference_date = fixed_date


@pytest.fixture(scope='session', autouse=True)
def reference_date(request):
"""Optionally replace date.today() for the duration of the test session."""
fixed_date = request.config._stdnum_reference_date
if fixed_date is None:
yield
return

real_date = datetime.date

class DateMeta(type):
"""Keep isinstance checks compatible with ordinary date objects."""

def __instancecheck__(cls, instance):
return isinstance(instance, real_date)

class ReferenceDate(real_date, metaclass=DateMeta):
"""Return ordinary dates, overriding only the current date."""

def __new__(cls, *args, **kwargs):
return real_date(*args, **kwargs)

@classmethod
def today(cls):
return fixed_date

with patch.object(datetime, 'date', ReferenceDate):
yield
78 changes: 78 additions & 0 deletions tests/test_reference_date.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# test_reference_date.py - test the optional reference date
#
# Copyright (C) 2026 Ryan Duguid
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, see <https://www.gnu.org/licenses/>.

"""Check the test reference date in isolated pytest processes."""

from pathlib import Path

import pytest


@pytest.fixture
def clock_suite(pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch) -> pytest.Pytester:
"""Install the real configuration in an isolated test directory."""
# These child suites test pytest configuration, not library coverage.
monkeypatch.delenv('COV_CORE_DATAFILE', raising=False)
source = Path(__file__).resolve().parent.parent / 'conftest.py'
pytester.makeconftest(source.read_text() + """
import datetime
_original_date = datetime.date

def pytest_sessionfinish(session):
assert datetime.date is _original_date
""")
pytester.makepyfile("""
import datetime
import os

real_date = datetime.date
real_today = datetime.date.today()

def test_date():
value = os.environ.get('STDNUM_TEST_DATE')
expected = real_date.fromisoformat(value) if value is not None else real_today
assert datetime.date.today() == expected
assert datetime.date(2000, 2, 29) == real_date(2000, 2, 29)
assert repr(datetime.date(2000, 2, 29)) == 'datetime.date(2000, 2, 29)'
assert isinstance(real_date(2000, 2, 29), datetime.date)
assert isinstance(datetime.datetime.now().date(), datetime.date)
if value is None:
assert datetime.date is real_date
""")
return pytester


def test_default_uses_real_date(clock_suite: pytest.Pytester, monkeypatch: pytest.MonkeyPatch) -> None:
"""No reference date leaves the date class and clock unchanged."""
monkeypatch.delenv('STDNUM_TEST_DATE', raising=False)
clock_suite.runpytest_subprocess('-q').assert_outcomes(passed=1)


@pytest.mark.parametrize('value', ['2024-01-01', '2040-04-24', '2024-02-29'])
def test_explicit_reference_date(clock_suite: pytest.Pytester, monkeypatch: pytest.MonkeyPatch, value: str) -> None:
"""A requested date applies during tests and is restored afterwards."""
monkeypatch.setenv('STDNUM_TEST_DATE', value)
clock_suite.runpytest_subprocess('-q').assert_outcomes(passed=1)


@pytest.mark.parametrize('value', ['', 'invalid', '2023-02-29', '20240101'])
def test_invalid_reference_date(clock_suite: pytest.Pytester, monkeypatch: pytest.MonkeyPatch, value: str) -> None:
"""Invalid dates fail with an actionable configuration error."""
monkeypatch.setenv('STDNUM_TEST_DATE', value)
result = clock_suite.runpytest_subprocess('-q')
assert result.ret == pytest.ExitCode.USAGE_ERROR
assert 'STDNUM_TEST_DATE must be a valid date in YYYY-MM-DD format' in result.stderr.str()
2 changes: 2 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ skip_missing_interpreters = true
deps = pytest
pytest-cov
commands = pytest
passenv = STDNUM_TEST_DATE
setenv=
PYTHONWARNINGS=all

Expand Down Expand Up @@ -33,6 +34,7 @@ setenv=
[testenv:mypy]
skip_install = true
deps = mypy<2.0 # required to keep Python 3.9 support
pytest<9 # pytest 9 syntax exceeds the Python 3.9 type-check target
types-requests
zeep
commands =
Expand Down
Loading