├── .flake8 ├── .github └── workflows │ ├── deploy.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── AUTHORS ├── CHANGELOG.rst ├── LICENSE ├── README.md ├── RELEASING.rst ├── pyproject.toml ├── setup.py ├── src └── pytest_datadir │ ├── __init__.py │ ├── plugin.py │ └── py.typed ├── tests ├── data │ ├── over.txt │ ├── shared_directory │ │ └── file.txt │ └── spam.txt ├── test_hello.py ├── test_hello │ ├── hello.txt │ ├── local_directory │ │ └── file.txt │ └── over.txt ├── test_nonexistent.py └── test_pathlib.py └── tox.ini /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length=88 3 | extend-ignore=E203,D104,D100,I004 4 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Release version' 8 | required: true 9 | default: '1.2.3' 10 | 11 | jobs: 12 | 13 | package: 14 | runs-on: ubuntu-latest 15 | env: 16 | SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.inputs.version }} 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Build and Check Package 22 | uses: hynek/build-and-inspect-python-package@v2.12.0 23 | 24 | deploy: 25 | needs: package 26 | runs-on: ubuntu-latest 27 | environment: deploy 28 | permissions: 29 | id-token: write # For PyPI trusted publishers. 30 | contents: write # For tag and release notes. 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | 35 | - name: Download Package 36 | uses: actions/download-artifact@v4 37 | with: 38 | name: Packages 39 | path: dist 40 | 41 | - name: Publish package to PyPI 42 | uses: pypa/gh-action-pypi-publish@v1.12.4 43 | 44 | - name: Push tag 45 | run: | 46 | git config user.name "pytest bot" 47 | git config user.email "pytestbot@gmail.com" 48 | git tag --annotate --message=v${{ github.event.inputs.version }} v${{ github.event.inputs.version }} ${{ github.sha }} 49 | git push origin v${{ github.event.inputs.version }} 50 | 51 | - name: GitHub Release 52 | uses: softprops/action-gh-release@v2 53 | with: 54 | files: dist/* 55 | tag_name: v${{ github.event.inputs.version }} 56 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - "test-me-*" 8 | 9 | pull_request: 10 | 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | 18 | package: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Build and Check Package 23 | uses: hynek/build-and-inspect-python-package@v2.12.0 24 | 25 | test: 26 | 27 | needs: [package] 28 | 29 | runs-on: ${{ matrix.os }} 30 | 31 | strategy: 32 | fail-fast: false 33 | matrix: 34 | python: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] 35 | os: [ubuntu-latest, windows-latest, macos-latest] 36 | include: 37 | - python: "3.8" 38 | tox_env: "py38" 39 | - python: "3.9" 40 | tox_env: "py39" 41 | - python: "3.10" 42 | tox_env: "py310" 43 | - python: "3.11" 44 | tox_env: "py311" 45 | - python: "3.12" 46 | tox_env: "py312" 47 | - python: "3.13" 48 | tox_env: "py313" 49 | 50 | steps: 51 | - uses: actions/checkout@v4 52 | 53 | - name: Download Package 54 | uses: actions/download-artifact@v4 55 | with: 56 | name: Packages 57 | path: dist 58 | 59 | - name: Set up Python 60 | uses: actions/setup-python@v5 61 | with: 62 | python-version: ${{ matrix.python }} 63 | 64 | - name: Install tox 65 | run: | 66 | python -m pip install --upgrade pip 67 | python -m pip install tox 68 | 69 | - name: Test 70 | shell: bash 71 | run: | 72 | tox run -e ${{ matrix.tox_env }} --installpkg `find dist/*.tar.gz` 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | .pytest_cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | # IDEs 61 | .idea/ 62 | 63 | /src/pytest_datadir/_version.py 64 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: (?x)(\.txt) 2 | repos: 3 | - repo: https://github.com/PyCQA/autoflake 4 | rev: v2.3.1 5 | hooks: 6 | - id: autoflake 7 | name: autoflake 8 | args: ["--in-place", "--remove-unused-variables", "--remove-all-unused-imports"] 9 | language: python 10 | files: \.py$ 11 | - repo: https://github.com/asottile/reorder-python-imports 12 | rev: v3.15.0 13 | hooks: 14 | - id: reorder-python-imports 15 | - repo: https://github.com/psf/black 16 | rev: 25.1.0 17 | hooks: 18 | - id: black 19 | args: [--safe, --quiet] 20 | - repo: https://github.com/asottile/blacken-docs 21 | rev: 1.19.1 22 | hooks: 23 | - id: blacken-docs 24 | additional_dependencies: [black==22.8.0] 25 | - repo: https://github.com/pre-commit/pre-commit-hooks 26 | rev: v5.0.0 27 | hooks: 28 | - id: trailing-whitespace 29 | - id: end-of-file-fixer 30 | - id: fix-encoding-pragma 31 | args: [--remove] 32 | - id: check-yaml 33 | exclude: tests 34 | - id: check-toml 35 | - id: check-json 36 | - id: check-merge-conflict 37 | - id: pretty-format-json 38 | args: [--autofix] 39 | - id: debug-statements 40 | language_version: python3 41 | - repo: https://github.com/PyCQA/flake8 42 | rev: 7.2.0 43 | hooks: 44 | - id: flake8 45 | exclude: tests/data 46 | language_version: python3 47 | additional_dependencies: 48 | - flake8-typing-imports==1.14.0 49 | - flake8-builtins==2.1.0 50 | - flake8-bugbear==23.1.20 51 | - repo: https://github.com/pre-commit/mirrors-mypy 52 | rev: v1.16.0 53 | hooks: 54 | - id: mypy 55 | files: ^(src/|tests/) 56 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Authors and contributors:: 2 | 3 | Bruno Oliveira 4 | Edison Gustavo Muenz 5 | Gabriel Reis 6 | Guilherme Quentel Melo 7 | Mauricio Lima 8 | Marcelo Duarte Trevisani 9 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | pytest-datadir 2 | ============== 3 | 4 | UNRELEASED 5 | ---------- 6 | 7 | *UNRELEASED* 8 | 9 | - Fix ``LazyDataDir.joinpath`` typing to also support ``Path`` objects as the right-hand side parameter. 10 | 11 | 1.7.2 12 | ----- 13 | 14 | *2025-06-06* 15 | 16 | - ``py.typed`` was added to the distribution, enabling users to use ``LazyDataDir`` in type annotations. 17 | 18 | 1.7.1 19 | ----- 20 | 21 | *2025-06-02* 22 | 23 | - Fixed bug using ``lazy_datadir`` to copy a file using a sub-directory (e.g, ``lazy_datadir / 'subdir' / 'file.txt'``) (`#99 `__). 24 | 25 | 1.7.0 26 | ----- 27 | 28 | *2025-05-30* 29 | 30 | - New `lazy_datadir` fixture that lazily copies files when accessed via `joinpath` or `/` operator. 31 | 32 | 33 | 1.6.1 34 | ----- 35 | 36 | *2025-02-07* 37 | 38 | - pytest 7.0+ is now required. 39 | 40 | 1.6.0 41 | ----- 42 | 43 | **Note**: this release has been yanked from PyPI due to `#89 `__. 44 | 45 | *2025-02-07* 46 | 47 | - Fixed compatibility with ``pytest-describe``. 48 | - ``original_datadir`` fixture is now ``module``-scoped. 49 | 50 | 1.5.0 (2023-10-02) 51 | ------------------ 52 | 53 | - Added support for Python 3.11 and 3.12. 54 | - Dropped support for Python 3.7. 55 | - Fix handling of UNC paths on Windows (`#33 `__). 56 | 57 | 1.4.1 (2022-10-24) 58 | ------------------ 59 | 60 | - Replace usage of ``tmpdir`` by ``tmp_path`` (`#48 `__). 61 | 62 | 63 | 1.4.0 (2022-01-18) 64 | ------------------ 65 | 66 | - Fix package so the ``LICENSE`` file is no longer in the root of the package. 67 | - Python 3.9 and 3.10 are now officially supported. 68 | - Python 2.7, 3.4 and 3.5 are no longer supported. 69 | 70 | 1.3.1 (2019-10-22) 71 | ------------------ 72 | 73 | - Add testing for Python 3.7 and 3.8. 74 | - Add ``python_requires`` to ``setup.py`` so ``pip`` will not try to install ``pytest-datadir`` in incompatible Python versions. 75 | 76 | 77 | 1.3.0 (2019-01-15) 78 | ------------------ 79 | 80 | - Add support for long path names on Windows (`#25 `__). 81 | 82 | 83 | 1.2.1 (2018-07-12) 84 | ------------------ 85 | 86 | - Fix ``pytest_datadir.version`` attribute to point to the correct version. 87 | 88 | 89 | 1.2.0 (2018-07-11) 90 | ------------------ 91 | 92 | - Use ``pathlib2`` on Python 2.7: this is the proper backport of Python 3's standard 93 | library. 94 | 95 | 1.1.0 (2018-07-10) 96 | ------------------ 97 | 98 | - If the data directory does not exist, the fixture will create an empty directory. 99 | 100 | 1.0.1 (2017-08-15) 101 | ------------------ 102 | 103 | **Fixes** 104 | 105 | - Fixed ``shared_datadir`` contents not being copied to a temp location on each test. `#12 106 | `_ 107 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is the MIT license: http://www.opensource.org/licenses/mit-license.php 2 | 3 | Copyright (c) 2015-2022 the pytest-datadir authors and contributors . 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | software and associated documentation files (the "Software"), to deal in the Software 7 | without restriction, including without limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 9 | to whom the Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or 12 | substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 16 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 17 | FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pytest-datadir 2 | 3 | pytest plugin for manipulating test data directories and files. 4 | 5 | [![Build Status](https://github.com/gabrielcnr/pytest-datadir/workflows/build/badge.svg?branch=master)](https://github.com/gabrielcnr/pytest-datadir/workflows/build/badge.svg?branch=master) 6 | [![PyPI](https://img.shields.io/pypi/v/pytest-datadir.svg)](https://pypi.python.org/pypi/pytest-datadir) 7 | [![CondaForge](https://img.shields.io/conda/vn/conda-forge/pytest-datadir.svg)](https://anaconda.org/conda-forge/pytest-datadir) 8 | ![Python Version](https://img.shields.io/badge/python-3.6+-blue.svg) 9 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 10 | 11 | 12 | # Usage 13 | 14 | `pytest-datadir` automatically looks for a directory matching your module's name or a global `data` folder. 15 | 16 | Consider the following directory structure: 17 | 18 | ``` 19 | . 20 | ├── data/ 21 | │   └── hello.txt 22 | ├── test_hello/ 23 | │   └── spam.txt 24 | └── test_hello.py 25 | ``` 26 | 27 | You can access file contents using the injected fixtures: 28 | 29 | - `datadir` (for module-specific `test_*` folders) 30 | - `shared_datadir` (for the global `data` folder) 31 | 32 | ```python 33 | def test_read_global(shared_datadir): 34 | contents = (shared_datadir / "hello.txt").read_text() 35 | assert contents == "Hello World!\n" 36 | 37 | 38 | def test_read_module(datadir): 39 | contents = (datadir / "spam.txt").read_text() 40 | assert contents == "eggs\n" 41 | ``` 42 | 43 | The contents of the data directory are copied to a temporary folder, ensuring safe file modifications without affecting other tests or original files. 44 | 45 | Both `datadir` and `shared_datadir` fixtures return `pathlib.Path` objects. 46 | 47 | ## lazy_datadir 48 | 49 | Version 1.7.0 introduced the `lazy_datadir` fixture, which only copies files and directories when accessed via the `joinpath` method or the `/` operator. 50 | 51 | ```python 52 | def test_read_module(lazy_datadir): 53 | contents = (lazy_datadir / "spam.txt").read_text() 54 | assert contents == "eggs\n" 55 | ``` 56 | 57 | Unlike `datadir`, `lazy_datadir` is an object that only implements `joinpath` and `/` operations. While not fully backward-compatible with `datadir`, most tests can switch to `lazy_datadir` without modifications. 58 | 59 | # License 60 | 61 | MIT. 62 | -------------------------------------------------------------------------------- /RELEASING.rst: -------------------------------------------------------------------------------- 1 | Here are the steps on how to make a new release. 2 | 3 | 1. Create a ``release-VERSION`` branch from ``upstream/master``. 4 | 2. Update ``CHANGELOG.rst``. 5 | 3. Push the branch to ``upstream``. 6 | 4. Once all tests pass, start the ``deploy`` workflow manually or via command-line:: 7 | 8 | gh workflow run deploy.yml --ref release-VERSION --field version=VERSION 9 | 10 | 5. Merge the PR. 11 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools >=61", "setuptools_scm[toml]>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pytest-datadir" 7 | authors = [ 8 | { name = "Gabriel Reis", email = "gabrielcnr@gmail.com" }, 9 | ] 10 | description = "pytest plugin for test data directories and files" 11 | keywords = ["pytest", "test", "unittest", "directory", "file"] 12 | license = { text = "MIT" } 13 | urls = {Homepage = "http://github.com/gabrielcnr/pytest-datadir"} 14 | classifiers = [ 15 | "Development Status :: 5 - Production/Stable", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: MIT License", 18 | "Operating System :: OS Independent", 19 | "Programming Language :: Python :: 3.8", 20 | "Programming Language :: Python :: 3.9", 21 | "Programming Language :: Python :: 3.10", 22 | "Programming Language :: Python :: 3.11", 23 | "Programming Language :: Python :: 3.12", 24 | "Topic :: Software Development :: Quality Assurance", 25 | "Topic :: Software Development :: Testing", 26 | ] 27 | dynamic = ["version"] 28 | requires-python = ">=3.8" 29 | dependencies = [ 30 | # Update tox.ini if the minimum required version changes. 31 | "pytest>=7.0", 32 | ] 33 | 34 | [project.entry-points.pytest11] 35 | pytest-datadir = "pytest_datadir.plugin" 36 | 37 | [project.readme] 38 | file = "README.md" 39 | content-type = "text/markdown" 40 | 41 | [project.optional-dependencies] 42 | testing = [ 43 | "pytest", 44 | "tox", 45 | ] 46 | dev = [ 47 | "pytest-datadir[testing]", 48 | "pre-commit", 49 | ] 50 | 51 | [tool.setuptools_scm] 52 | write_to = "src/pytest_datadir/_version.py" 53 | 54 | [tool.setuptools.packages.find] 55 | include = ["pytest_datadir", "pytest_datadir.*"] 56 | where = ["src"] 57 | 58 | [tool.pytest.ini_options] 59 | testpaths = ["tests"] 60 | 61 | [tool.setuptools.package-data] 62 | "pytest_datadir" = ["py.typed"] 63 | 64 | [tool.mypy] 65 | disallow_any_generics = true 66 | disallow_subclassing_any = true 67 | disallow_untyped_defs = true 68 | ignore_missing_imports = true 69 | no_implicit_optional = true 70 | pretty = true 71 | show_error_codes = true 72 | strict_equality = true 73 | warn_redundant_casts = true 74 | warn_return_any = true 75 | warn_unreachable = true 76 | warn_unused_configs = true 77 | warn_unused_ignores = true 78 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | # Please check pyproject.toml for metadata 4 | setup() 5 | -------------------------------------------------------------------------------- /src/pytest_datadir/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | from ._version import version 3 | 4 | __version__ = version 5 | except ImportError: 6 | __version__ = version = "unknown" 7 | -------------------------------------------------------------------------------- /src/pytest_datadir/plugin.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import sys 4 | from dataclasses import dataclass 5 | from pathlib import Path 6 | from typing import Union 7 | 8 | import pytest 9 | 10 | 11 | def _win32_longpath(path: str) -> str: 12 | """ 13 | Helper function to add the long path prefix for Windows, so that shutil.copytree 14 | won't fail while working with paths with 255+ chars. 15 | """ 16 | if sys.platform == "win32": 17 | # The use of os.path.normpath here is necessary since "the "\\?\" prefix 18 | # to a path string tells the Windows APIs to disable all string parsing 19 | # and to send the string that follows it straight to the file system". 20 | # (See https://docs.microsoft.com/pt-br/windows/desktop/FileIO/naming-a-file) 21 | normalized = os.path.normpath(path) 22 | if not normalized.startswith("\\\\?\\"): 23 | is_unc = normalized.startswith("\\\\") 24 | # see https://en.wikipedia.org/wiki/Path_(computing)#Universal_Naming_Convention # noqa: E501 25 | if ( 26 | is_unc 27 | ): # then we need to insert an additional "UNC\" to the longpath prefix 28 | normalized = normalized.replace("\\\\", "\\\\?\\UNC\\") 29 | else: 30 | normalized = "\\\\?\\" + normalized 31 | return normalized 32 | else: 33 | return path 34 | 35 | 36 | @pytest.fixture 37 | def shared_datadir(request: pytest.FixtureRequest, tmp_path: Path) -> Path: 38 | original_shared_path = os.path.join(request.fspath.dirname, "data") 39 | temp_path = tmp_path / "data" 40 | shutil.copytree( 41 | _win32_longpath(original_shared_path), _win32_longpath(str(temp_path)) 42 | ) 43 | return temp_path 44 | 45 | 46 | @pytest.fixture(scope="module") 47 | def original_datadir(request: pytest.FixtureRequest) -> Path: 48 | return Path(request.path).with_suffix("") 49 | 50 | 51 | @pytest.fixture 52 | def datadir(original_datadir: Path, tmp_path: Path) -> Path: 53 | result = tmp_path / original_datadir.stem 54 | if original_datadir.is_dir(): 55 | shutil.copytree( 56 | _win32_longpath(str(original_datadir)), _win32_longpath(str(result)) 57 | ) 58 | else: 59 | result.mkdir() 60 | return result 61 | 62 | 63 | @dataclass(frozen=True) 64 | class LazyDataDir: 65 | """ 66 | A dataclass to represent a lazy data directory. 67 | 68 | Unlike the datadir fixture, this class copies files and directories to the 69 | temporary directory when requested via the `joinpath` method or the `/` operator. 70 | """ 71 | 72 | original_datadir: Path 73 | tmp_path: Path 74 | 75 | def joinpath(self, other: Union[Path, str]) -> Path: 76 | """ 77 | Return `other` joined with the temporary directory. 78 | 79 | If `other` exists in the data directory, the corresponding file or directory is 80 | copied to the temporary directory before being returned. 81 | 82 | Note that the file or directory is only copied once per test. Subsequent calls 83 | with the same argument within the same test will not trigger another copy. 84 | """ 85 | original = self.original_datadir / other 86 | target = self.tmp_path / other 87 | if original.exists() and not target.exists(): 88 | if original.is_file(): 89 | target.parent.mkdir(parents=True, exist_ok=True) 90 | shutil.copy( 91 | _win32_longpath(str(original)), _win32_longpath(str(target)) 92 | ) 93 | elif original.is_dir(): 94 | shutil.copytree( 95 | _win32_longpath(str(original)), _win32_longpath(str(target)) 96 | ) 97 | return target 98 | 99 | def __truediv__(self, other: Union[Path, str]) -> Path: 100 | return self.joinpath(other) 101 | 102 | 103 | @pytest.fixture 104 | def lazy_datadir(original_datadir: Path, tmp_path: Path) -> LazyDataDir: 105 | """ 106 | Return a lazy data directory. 107 | 108 | Here, "lazy" means that the temporary directory is initially created empty. 109 | 110 | Files and directories are then copied from the data directory only when first 111 | accessed via the ``joinpath`` method or the ``/`` operator. 112 | 113 | Args: 114 | original_datadir: The original data directory. 115 | tmp_path: Pytest's built-in fixture providing a temporary directory path. 116 | """ 117 | return LazyDataDir(original_datadir, tmp_path) 118 | -------------------------------------------------------------------------------- /src/pytest_datadir/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielcnr/pytest-datadir/aa04a01f5c387d2077c89ce2e0c12a88d2651a89/src/pytest_datadir/py.typed -------------------------------------------------------------------------------- /tests/data/over.txt: -------------------------------------------------------------------------------- 1 | 8000 -------------------------------------------------------------------------------- /tests/data/shared_directory/file.txt: -------------------------------------------------------------------------------- 1 | global contents -------------------------------------------------------------------------------- /tests/data/spam.txt: -------------------------------------------------------------------------------- 1 | eggs 2 | -------------------------------------------------------------------------------- /tests/test_hello.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | import pytest 5 | from pytest_datadir.plugin import LazyDataDir 6 | 7 | 8 | @pytest.fixture(autouse=True, scope="module") 9 | def create_long_file_path() -> None: 10 | """ 11 | Create a very long file name to ensure datadir can copy it correctly. 12 | 13 | We don't just create this file in the repository because this makes it 14 | problematic to clone on Windows without LongPaths enabled in the system. 15 | """ 16 | d = Path(__file__).with_suffix("") 17 | old_cwd = os.getcwd() 18 | try: 19 | os.chdir(d) 20 | Path("a" * 250 + ".txt").touch() 21 | finally: 22 | os.chdir(old_cwd) 23 | 24 | 25 | def test_read_hello(datadir: Path) -> None: 26 | assert set(os.listdir(str(datadir))) == { 27 | "local_directory", 28 | "hello.txt", 29 | "over.txt", 30 | "a" * 250 + ".txt", 31 | } 32 | with (datadir / "hello.txt").open() as fp: 33 | contents = fp.read() 34 | assert contents == "Hello, world!\n" 35 | 36 | 37 | def test_change_test_files( 38 | datadir: Path, 39 | original_datadir: Path, 40 | shared_datadir: Path, 41 | request: pytest.FixtureRequest, 42 | ) -> None: 43 | filename = datadir / "hello.txt" 44 | with filename.open("w") as fp: 45 | fp.write("Modified text!\n") 46 | 47 | original_filename = original_datadir / "hello.txt" 48 | with original_filename.open() as fp: 49 | assert fp.read() == "Hello, world!\n" 50 | 51 | with filename.open() as fp: 52 | assert fp.read() == "Modified text!\n" 53 | 54 | shared_filename = shared_datadir / "over.txt" 55 | with shared_filename.open("w") as fp: 56 | fp.write("changed") 57 | shared_original = os.path.join(request.fspath.dirname, "data", "over.txt") 58 | with open(shared_original) as fp: 59 | assert fp.read().strip() == "8000" 60 | 61 | 62 | def test_read_spam_from_other_dir(shared_datadir: Path) -> None: 63 | filename = shared_datadir / "spam.txt" 64 | with filename.open() as fp: 65 | contents = fp.read() 66 | assert contents == "eggs\n" 67 | 68 | 69 | def test_file_override(shared_datadir: Path, datadir: Path) -> None: 70 | """The same file is in the module dir and global data. 71 | Shared files are kept in a different temp directory""" 72 | shared_filepath = shared_datadir / "over.txt" 73 | private_filepath = datadir / "over.txt" 74 | assert shared_filepath.is_file() 75 | assert private_filepath.is_file() 76 | assert shared_filepath != private_filepath 77 | 78 | 79 | def test_local_directory(datadir: Path) -> None: 80 | directory = datadir / "local_directory" 81 | assert directory.is_dir() 82 | filename = directory / "file.txt" 83 | assert filename.is_file() 84 | with filename.open() as fp: 85 | contents = fp.read() 86 | assert contents.strip() == "local contents" 87 | 88 | 89 | def test_shared_directory(shared_datadir: Path) -> None: 90 | assert shared_datadir.is_dir() 91 | filename = shared_datadir / "shared_directory" / "file.txt" 92 | assert filename.is_file() 93 | with filename.open() as fp: 94 | contents = fp.read() 95 | assert contents.strip() == "global contents" 96 | 97 | 98 | def test_lazy_copy(lazy_datadir: LazyDataDir) -> None: 99 | # The temporary directory starts empty. 100 | assert {x.name for x in lazy_datadir.tmp_path.iterdir()} == set() 101 | 102 | # Lazy copy file. 103 | hello = lazy_datadir / "hello.txt" 104 | assert {x.name for x in lazy_datadir.tmp_path.iterdir()} == {"hello.txt"} 105 | assert hello.read_text() == "Hello, world!\n" 106 | 107 | # Accessing the same file multiple times does not copy the file again. 108 | hello.write_text("Hello world, hello world.") 109 | hello = lazy_datadir / Path("hello.txt") 110 | assert hello.read_text() == "Hello world, hello world." 111 | 112 | # Lazy copy data directory. 113 | local_dir = lazy_datadir / "local_directory" 114 | assert {x.name for x in lazy_datadir.tmp_path.iterdir()} == { 115 | "hello.txt", 116 | "local_directory", 117 | } 118 | assert local_dir.is_dir() is True 119 | assert local_dir.joinpath("file.txt").read_text() == "local contents" 120 | 121 | # It is OK to request a file that does not exist in the data directory. 122 | fn = lazy_datadir / "new-file.txt" 123 | assert fn.exists() is False 124 | fn.write_text("new contents") 125 | assert {x.name for x in lazy_datadir.tmp_path.iterdir()} == { 126 | "hello.txt", 127 | "local_directory", 128 | "new-file.txt", 129 | } 130 | 131 | 132 | def test_lazy_copy_sub_directory(lazy_datadir: LazyDataDir) -> None: 133 | """Copy via file by using a sub-directory (#99).""" 134 | # The temporary directory starts empty. 135 | assert {x.name for x in lazy_datadir.tmp_path.iterdir()} == set() 136 | 137 | # Lazy copy file in a sub-directory. 138 | fn = lazy_datadir / "local_directory/file.txt" 139 | assert {x.name for x in lazy_datadir.tmp_path.iterdir()} == { 140 | "local_directory", 141 | } 142 | assert fn.read_text() == "local contents" 143 | -------------------------------------------------------------------------------- /tests/test_hello/hello.txt: -------------------------------------------------------------------------------- 1 | Hello, world! 2 | -------------------------------------------------------------------------------- /tests/test_hello/local_directory/file.txt: -------------------------------------------------------------------------------- 1 | local contents -------------------------------------------------------------------------------- /tests/test_hello/over.txt: -------------------------------------------------------------------------------- 1 | 9000 -------------------------------------------------------------------------------- /tests/test_nonexistent.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | def test_missing_data_dir_starts_empty(datadir: Path) -> None: 5 | assert list(datadir.iterdir()) == [] 6 | -------------------------------------------------------------------------------- /tests/test_pathlib.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | 4 | import pytest 5 | from pytest_datadir.plugin import _win32_longpath 6 | 7 | 8 | def test_win32_longpath_idempotent(datadir: Path) -> None: 9 | """Double application should not prepend twice.""" 10 | first = _win32_longpath(str(datadir)) 11 | second = _win32_longpath(first) 12 | assert first == second 13 | 14 | 15 | @pytest.mark.skipif( 16 | not sys.platform.startswith("win"), reason="Only makes sense on Windows" 17 | ) 18 | def test_win32_longpath_unc(datadir: Path) -> None: 19 | unc_path = r"\\ComputerName\SharedFolder\Resource" 20 | longpath = _win32_longpath(unc_path) 21 | assert longpath.startswith("\\\\?\\UNC\\") 22 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{38,39,310,311,312} 3 | 4 | [testenv] 5 | deps = 6 | # Test with minimum required pytest version. 7 | py38: pytest==7.0.0 8 | commands= 9 | pytest --color=yes {posargs:tests} 10 | --------------------------------------------------------------------------------