├── .github └── workflows │ ├── check-pull-request.yml │ ├── check-push.yml │ └── publish-to-pypi.yml ├── .gitignore ├── CHANGELOG.rst ├── LICENSE.txt ├── README.rst ├── precommit.py ├── pylint.rc ├── setup.py ├── style.yapf ├── temppathlib ├── __init__.py └── py.typed ├── tests ├── __init__.py └── test_temppathlib.py └── tox.ini /.github/workflows/check-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Check-pull-request 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | Execute: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] 11 | 12 | steps: 13 | - uses: actions/checkout@master 14 | 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | 20 | - name: Install dependencies 21 | run: | 22 | python3 -m pip install --upgrade pip 23 | pip3 install --upgrade coveralls 24 | pip3 install -e .[dev] 25 | 26 | - name: Run checks 27 | run: ./precommit.py 28 | 29 | - name: Upload Coverage 30 | run: coveralls --service=github 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | COVERALLS_FLAG_NAME: ${{ matrix.python-version }} 34 | COVERALLS_PARALLEL: true 35 | 36 | Finish-Coveralls: 37 | name: Finish Coveralls 38 | needs: Execute 39 | runs-on: ubuntu-latest 40 | container: python:3-slim 41 | steps: 42 | - name: Finish Coveralls 43 | run: | 44 | pip3 install --upgrade coveralls 45 | coveralls --finish --service=github 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | -------------------------------------------------------------------------------- /.github/workflows/check-push.yml: -------------------------------------------------------------------------------- 1 | name: Check-push 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | Execute: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] 14 | 15 | steps: 16 | - uses: actions/checkout@master 17 | 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | 23 | - name: Install dependencies 24 | run: | 25 | python3 -m pip install --upgrade pip 26 | pip3 install -e .[dev] 27 | pip3 install coveralls 28 | 29 | - name: Run checks 30 | run: ./precommit.py 31 | 32 | - name: Upload coverage to coveralls 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | run: coveralls --service=github 36 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-pypi.yml: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/actions/starter-workflows/blob/main/ci/python-publish.yml 2 | name: Publish to Pypi 3 | 4 | on: 5 | release: 6 | types: [created] 7 | 8 | push: 9 | branches: 10 | - '*/fixed-publishing-to-pypi' 11 | 12 | jobs: 13 | deploy: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Set up Python 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: '3.x' 24 | 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install setuptools wheel twine 29 | 30 | - name: Build and publish 31 | env: 32 | TWINE_USERNAME: "__token__" 33 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 34 | run: | 35 | python setup.py sdist 36 | twine upload dist/* 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv3 2 | .mypy_cache/ 3 | .idea 4 | *.pyc 5 | .precommit_hashes 6 | *.egg-info 7 | .tox 8 | dist/ 9 | venv/ 10 | .coverage 11 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 1.2.0 2 | ===== 3 | * Revisited the supported Python versions up to 3.10 (#13) 4 | * Supported prefix and suffix in ``TmpDirIfNecessary`` (#12) 5 | 6 | 1.1.0 7 | ===== 8 | * Wrapped ``tempfile.gettempdir`` (#7) 9 | 10 | 1.0.4 11 | ===== 12 | * Added support for Python 3.6, 3.7 and 3.8 13 | * Made ``path`` a getter so that the return type is non-None 14 | 15 | 1.0.3 16 | ===== 17 | * Added py.typed to the distribution so that the library can be used with mypy 18 | 19 | 1.0.2 20 | ===== 21 | * Moved from bitbucket to github 22 | * Moved the temppathlib.py to temppathlib/__init__.py to facilitate usage with site packages and mypy 23 | 24 | 1.0.1 25 | ===== 26 | * Fixed NamedTemporaryFile to accept None as ``dir`` argument. 27 | 28 | 1.0.0 29 | ===== 30 | * Initial version -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Parquery AG 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | temppathlib 2 | =========== 3 | 4 | .. image:: https://github.com/Parquery/temppathlib/workflows/Check-push/badge.svg 5 | :target: https://github.com/Parquery/temppathlib/actions?query=workflow%3ACheck-push 6 | :alt: Check status 7 | 8 | .. image:: https://coveralls.io/repos/github/Parquery/temppathlib/badge.svg?branch=master 9 | :target: https://coveralls.io/github/Parquery/temppathlib 10 | :alt: Test coverage 11 | 12 | .. image:: https://badge.fury.io/py/temppathlib.svg 13 | :target: https://pypi.org/project/temppathlib/ 14 | :alt: PyPI - version 15 | 16 | .. image:: https://img.shields.io/pypi/pyversions/temppathlib.svg 17 | :target: https://pypi.org/project/temppathlib/ 18 | :alt: PyPI - Python Version 19 | 20 | Temppathlib provides wrappers around ``tempfile`` so that you can directly use them together with ``pathlib`` module. 21 | We found it cumbersome to convert ``tempfile`` objects manually to ``pathlib.Path`` whenever we needed a temporary 22 | file. 23 | 24 | Additionally, we also provide: 25 | 26 | * a context manager ``removing_tree`` that checks if a path exists and recursively deletes it 27 | by wrapping ``shutil.rmtree``. 28 | 29 | * a context manager ``TmpDirIfNecessary`` that creates a temporary directory if no directory is given and otherwise 30 | uses a supplied directory. This is useful when you want to keep some of the temporary files for examination 31 | after the program finished. We usually specify an optional ``--operation_dir`` command-line argument to our programs 32 | and pass its value to the ``TmpDirIfNecessary``. 33 | 34 | If you need a more complex library to transition from string paths to ``pathlib.Path``, have a look at 35 | ruamel.std.pathlib_. 36 | 37 | .. _ruamel.std.pathlib: https://pypi.org/project/ruamel.std.pathlib/ 38 | 39 | Usage 40 | ===== 41 | .. code-block:: python 42 | 43 | import pathlib 44 | 45 | import temppathlib 46 | 47 | # create a temporary directory 48 | with temppathlib.TemporaryDirectory() as tmp_dir: 49 | tmp_pth = tmp_dir.path / "some-filename.txt" 50 | # do something else with tmp_dir ... 51 | 52 | # create a temporary file 53 | with temppathlib.NamedTemporaryFile() as tmp: 54 | # write to it 55 | tmp.file.write('hello'.encode()) 56 | tmp.file.flush() 57 | 58 | # you can use its path. 59 | target_pth = pathlib.Path('/some/permanent/directory') / tmp.path.name 60 | 61 | # create a temporary directory only if necessary 62 | operation_dir = pathlib.Path("/some/operation/directory) 63 | with temppathlib.TmpDirIfNecessary(path=operation_dir) as op_dir: 64 | # do something with the operation directory 65 | pth = op_dir.path / "some-file.txt" 66 | 67 | # operation_dir is not deleted since 'path' was specified. 68 | 69 | 70 | with temppathlib.TmpDirIfNecessary() as op_dir: 71 | # do something with the operation directory 72 | pth = op_dir.path / "some-file.txt" 73 | 74 | # op_dir is deleted since 'path' argument was not specified. 75 | 76 | # context manager to remove the path recursively 77 | pth = pathlib.Path('/some/directory') 78 | with temppathlib.removing_tree(pth): 79 | # do something in the directory ... 80 | pass 81 | 82 | Installation 83 | ============ 84 | 85 | * Create a virtual environment: 86 | 87 | .. code-block:: bash 88 | 89 | python3 -m venv venv3 90 | 91 | * Activate it: 92 | 93 | .. code-block:: bash 94 | 95 | source venv3/bin/activate 96 | 97 | * Install temppathlib with pip: 98 | 99 | .. code-block:: bash 100 | 101 | pip3 install temppathlib 102 | 103 | Development 104 | =========== 105 | 106 | * Check out the repository. 107 | 108 | * In the repository root, create the virtual environment: 109 | 110 | .. code-block:: bash 111 | 112 | python3 -m venv venv3 113 | 114 | * Activate the virtual environment: 115 | 116 | .. code-block:: bash 117 | 118 | source venv3/bin/activate 119 | 120 | * Install the development dependencies: 121 | 122 | .. code-block:: bash 123 | 124 | pip3 install -e .[dev] 125 | 126 | * We use tox for testing and packaging the distribution. Assuming that the virtual environment has been activated and 127 | the development dependencies have been installed, run: 128 | 129 | .. code-block:: bash 130 | 131 | tox 132 | 133 | * We also provide a set of pre-commit checks that lint and check code for formatting. Run them locally from an activated 134 | virtual environment with development dependencies: 135 | 136 | .. code-block:: bash 137 | 138 | ./precommit.py 139 | 140 | * The pre-commit script can also automatically format the code: 141 | 142 | .. code-block:: bash 143 | 144 | ./precommit.py --overwrite 145 | 146 | Versioning 147 | ========== 148 | We follow `Semantic Versioning `_. The version X.Y.Z indicates: 149 | 150 | * X is the major version (backward-incompatible), 151 | * Y is the minor version (backward-compatible), and 152 | * Z is the patch version (backward-compatible bug fix). -------------------------------------------------------------------------------- /precommit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Run precommit checks on the repository.""" 3 | import argparse 4 | import os 5 | import pathlib 6 | import re 7 | import subprocess 8 | import sys 9 | 10 | 11 | def main() -> int: 12 | """Execute the main routine.""" 13 | parser = argparse.ArgumentParser() 14 | parser.add_argument( 15 | "--overwrite", 16 | help="Overwrites the unformatted source files with the " 17 | "well-formatted code in place. If not set, " 18 | "an exception is raised if any of the files do not conform " 19 | "to the style guide.", 20 | action='store_true') 21 | 22 | args = parser.parse_args() 23 | 24 | overwrite = bool(args.overwrite) 25 | 26 | repo_root = pathlib.Path(__file__).parent 27 | 28 | # yapf: disable 29 | source_files = ( 30 | sorted((repo_root / "temppathlib").glob("**/*.py")) + 31 | sorted((repo_root / "tests").glob("**/*.py"))) 32 | # yapf: enable 33 | 34 | if overwrite: 35 | print('Removing trailing whitespace...') 36 | for pth in source_files: 37 | pth.write_text(re.sub(r'[ \t]+$', '', pth.read_text(), flags=re.MULTILINE)) 38 | 39 | print("YAPF'ing...") 40 | yapf_targets = ["tests", "temppathlib", "setup.py", "precommit.py"] 41 | if overwrite: 42 | # yapf: disable 43 | subprocess.check_call( 44 | ["yapf", "--in-place", "--style=style.yapf", "--recursive"] + 45 | yapf_targets, 46 | cwd=str(repo_root)) 47 | # yapf: enable 48 | else: 49 | # yapf: disable 50 | subprocess.check_call( 51 | ["yapf", "--diff", "--style=style.yapf", "--recursive"] + 52 | yapf_targets, 53 | cwd=str(repo_root)) 54 | # yapf: enable 55 | 56 | print("Mypy'ing...") 57 | subprocess.check_call(["mypy", "--strict", "temppathlib", "tests"], cwd=str(repo_root)) 58 | 59 | print("Isort'ing...") 60 | # yapf: disable 61 | isort_files = map(str, source_files) 62 | # yapf: enable 63 | 64 | # yapf: disable 65 | subprocess.check_call( 66 | ["isort", "--project", "temppathlib", '--line-width', '120'] + 67 | ([] if overwrite else ['--check-only']) + 68 | [str(pth) for pth in source_files]) 69 | # yapf: enable 70 | 71 | print("Pydocstyle'ing...") 72 | subprocess.check_call(["pydocstyle", "temppathlib"], cwd=str(repo_root)) 73 | 74 | print("Pylint'ing...") 75 | subprocess.check_call(["pylint", "--rcfile=pylint.rc", "tests", "temppathlib"], cwd=str(repo_root)) 76 | 77 | print("Testing...") 78 | env = os.environ.copy() 79 | env['ICONTRACT_SLOW'] = 'true' 80 | 81 | # yapf: disable 82 | subprocess.check_call( 83 | ["coverage", "run", 84 | "--source", "temppathlib", 85 | "-m", "unittest", "discover", "tests"], 86 | cwd=str(repo_root), 87 | env=env) 88 | # yapf: enable 89 | 90 | subprocess.check_call(["coverage", "report"]) 91 | 92 | print("Doctesting...") 93 | doctest_files = ([repo_root / "README.rst"] + sorted((repo_root / "temppathlib").glob("**/*.py"))) 94 | 95 | for pth in doctest_files: 96 | subprocess.check_call([sys.executable, "-m", "doctest", str(pth)]) 97 | 98 | print("Checking setup.py sdist ...") 99 | subprocess.check_call([sys.executable, "setup.py", "sdist"], cwd=str(repo_root)) 100 | 101 | print("Checking with twine...") 102 | subprocess.check_call(["twine", "check", "dist/*"], cwd=str(repo_root)) 103 | 104 | return 0 105 | 106 | 107 | if __name__ == "__main__": 108 | sys.exit(main()) 109 | -------------------------------------------------------------------------------- /pylint.rc: -------------------------------------------------------------------------------- 1 | [TYPECHECK] 2 | ignored-modules = numpy 3 | ignored-classes = numpy,PurePath 4 | generated-members=bottle\.request\.forms\.decode,bottle\.request\.query\.decode 5 | 6 | [FORMAT] 7 | max-line-length=120 8 | 9 | [MESSAGES CONTROL] 10 | disable=too-few-public-methods,abstract-class-little-used,len-as-condition,bad-continuation,bad-whitespace,too-many-arguments 11 | 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """A setuptools based setup module. 2 | 3 | See: 4 | https://packaging.python.org/en/latest/distributing.html 5 | https://github.com/pypa/sampleproject 6 | """ 7 | import os 8 | 9 | from setuptools import setup, find_packages 10 | 11 | # pylint: disable=redefined-builtin 12 | 13 | here = os.path.abspath(os.path.dirname(__file__)) # pylint: disable=invalid-name 14 | 15 | with open(os.path.join(here, 'README.rst'), encoding='utf-8') as f: 16 | long_description = f.read() # pylint: disable=invalid-name 17 | 18 | setup( 19 | name='temppathlib', 20 | version='1.2.0', # don't forget to update the changelog! 21 | description='Wrap tempfile to give you pathlib.Path.', 22 | long_description=long_description, 23 | url='https://github.com/Parquery/temppathlib', 24 | author='Marko Ristin', 25 | author_email='marko@parquery.com', 26 | classifiers=[ 27 | # yapf: disable 28 | 'Development Status :: 5 - Production/Stable', 29 | 'Intended Audience :: Developers', 30 | 'License :: OSI Approved :: MIT License', 31 | 'Programming Language :: Python :: 3.6', 32 | 'Programming Language :: Python :: 3.7', 33 | 'Programming Language :: Python :: 3.8', 34 | 'Programming Language :: Python :: 3.9', 35 | 'Programming Language :: Python :: 3.10', 36 | # yapf: enable 37 | ], 38 | keywords='tempfile pathlib temporary file directory mkdtemp mkstemp', 39 | packages=find_packages(exclude=['tests']), 40 | install_requires=None, 41 | extras_require={ 42 | 'dev': [ 43 | # yapf: disable 44 | 'mypy==0.941', 45 | 'pylint==2.12.2', 46 | 'yapf==0.20.2', 47 | 'tox>=3,<4', 48 | 'coverage>=5,<6', 49 | 'pydocstyle>=5,<6', 50 | 'twine' 51 | # yapf: enable 52 | ], 53 | }, 54 | py_modules=['temppathlib'], 55 | package_data={"temppathlib": ["py.typed"]}, 56 | ) 57 | -------------------------------------------------------------------------------- /style.yapf: -------------------------------------------------------------------------------- 1 | [style] 2 | based_on_style = pep8 3 | spaces_before_comment = 2 4 | split_before_logical_operator = true 5 | column_limit = 120 6 | coalesce_brackets = true 7 | 8 | -------------------------------------------------------------------------------- /temppathlib/__init__.py: -------------------------------------------------------------------------------- 1 | """Wrap tempfile to give you pathlib.Path.""" 2 | 3 | import pathlib 4 | import shutil 5 | import tempfile 6 | from typing import IO, Any, Optional, Union # pylint: disable=unused-import 7 | 8 | 9 | class removing_tree: # pylint: disable=invalid-name 10 | """Check if the path exists, and if it does, calls shutil.rmtree on it.""" 11 | 12 | def __init__(self, path: Union[str, pathlib.Path]) -> None: 13 | """Initialize with the given value.""" 14 | if isinstance(path, str): 15 | self.path = pathlib.Path(path) 16 | elif isinstance(path, pathlib.Path): 17 | self.path = path 18 | else: 19 | raise ValueError(f"Unexpected type of 'path': {type(path)}") 20 | 21 | def __enter__(self) -> pathlib.Path: 22 | """Give back the path that will be removed.""" 23 | return self.path 24 | 25 | def __exit__(self, exc_type, exc_val, exc_tb) -> None: # type: ignore 26 | """Remove the path if it exists.""" 27 | if self.path.exists(): 28 | shutil.rmtree(str(self.path)) 29 | 30 | 31 | class TmpDirIfNecessary: 32 | """ 33 | Forward the directory path (if defined) or create a temporary directory. 34 | 35 | If dont_delete_tmp_dir is set to True, the temporary directory is not deleted on exit. 36 | 37 | The directory (be it a temporary or not) is created on enter. If the path was not specified (and a temporary 38 | directory needs to be created), its name is generated only on enter. 39 | """ 40 | 41 | def __init__(self, 42 | path: Union[None, str, pathlib.Path], 43 | base_tmp_dir: Union[None, str, pathlib.Path] = None, 44 | dont_delete_tmp_dir: bool = False, 45 | prefix: Optional[str] = None, 46 | suffix: Optional[str] = None) -> None: 47 | """ 48 | Initialize with the given values. 49 | 50 | :param path: 51 | provided path to the directory; if specified, no temporary directory is created. 52 | 53 | :param base_tmp_dir: 54 | parent directory of the temporary directories; if not set, 55 | the default is used (usually '/tmp'). This path is only used if a temporary directory needs to be created 56 | and has no effect if 'path' was provided. 57 | 58 | :param dont_delete_tmp_dir: 59 | if set, the temporary directory is not deleted upon close. 60 | 61 | If the 'path' was provided, this argument has no effect. 62 | 63 | :param prefix: 64 | If 'prefix' is not None, the name will begin with that prefix, 65 | otherwise a default prefix is used. 66 | 67 | :param suffix: 68 | If 'suffix' is not None, the name will end with that suffix, 69 | otherwise a default suffix is used. 70 | """ 71 | if base_tmp_dir is None: 72 | self.base_tmp_dir = base_tmp_dir 73 | elif isinstance(base_tmp_dir, pathlib.Path): 74 | self.base_tmp_dir = base_tmp_dir 75 | elif isinstance(base_tmp_dir, str): 76 | self.base_tmp_dir = pathlib.Path(base_tmp_dir) 77 | else: 78 | raise ValueError(f"Unexpected type of 'base_tmp_dir': {type(base_tmp_dir)}") 79 | 80 | self._path = None # type: Optional[pathlib.Path] 81 | 82 | if path is None: 83 | pass 84 | elif isinstance(path, str): 85 | self._path = pathlib.Path(path) 86 | elif isinstance(path, pathlib.Path): 87 | self._path = path 88 | else: 89 | raise ValueError(f"Unexpected type for the argument `path`: {type(path)}") 90 | 91 | self.dont_delete = dont_delete_tmp_dir 92 | 93 | self._prefix = prefix 94 | self._suffix = suffix 95 | 96 | self.__use_tmp_dir = path is None 97 | 98 | self.exited = False 99 | 100 | @property 101 | def path(self) -> pathlib.Path: 102 | """Get the underlying path or raise if the path has not been set.""" 103 | if self._path is None: 104 | raise RuntimeError("The _path has not been set. " 105 | f"Are you using {self.__class__.__name__} outside of the context management?") 106 | 107 | return self._path 108 | 109 | def __enter__(self) -> 'TmpDirIfNecessary': 110 | """Create the temporary directory if necessary.""" 111 | if self.exited: 112 | raise RuntimeError("Already exited") 113 | 114 | if self._path is None: 115 | if self.base_tmp_dir is None: 116 | self._path = pathlib.Path(tempfile.mkdtemp(prefix=self._prefix, suffix=self._suffix)) 117 | else: 118 | self._path = pathlib.Path( 119 | tempfile.mkdtemp(dir=str(self.base_tmp_dir), prefix=self._prefix, suffix=self._suffix)) 120 | else: 121 | self._path.mkdir(exist_ok=True, parents=True) 122 | 123 | return self 124 | 125 | def __exit__(self, exc_type, exc_val, exc_tb): # type: ignore 126 | """Remove the directory if dont_delete has not been set.""" 127 | if self.__use_tmp_dir and not self.dont_delete: 128 | shutil.rmtree(str(self._path)) 129 | 130 | 131 | class TemporaryDirectory: 132 | """ 133 | Create a temporary directory and deletes it on exit. 134 | 135 | The path to the temporary directory is generated and the directory is created only on __enter__. 136 | """ 137 | 138 | def __init__(self, 139 | base_tmp_dir: Union[None, str, pathlib.Path] = None, 140 | prefix: Optional[str] = None, 141 | dont_delete: bool = False) -> None: 142 | """ 143 | Initialize with the given values. 144 | 145 | :param base_tmp_dir: if specified, this directory will be used as the parent of the temporary directory. 146 | :param prefix: if specified, the prefix of the directory name 147 | :param dont_delete: if set, the directory is not deleted upon close(). 148 | """ 149 | self.exited = False 150 | self._path = None # type: Optional[pathlib.Path] 151 | 152 | if base_tmp_dir is None: 153 | self.base_tmp_dir = base_tmp_dir 154 | elif isinstance(base_tmp_dir, pathlib.Path): 155 | self.base_tmp_dir = base_tmp_dir 156 | elif isinstance(base_tmp_dir, str): 157 | self.base_tmp_dir = pathlib.Path(base_tmp_dir) 158 | else: 159 | raise ValueError(f"Unexpected type of 'base_tmp_dir': {type(base_tmp_dir)}") 160 | 161 | self.prefix = prefix 162 | self.dont_delete = dont_delete 163 | 164 | def __enter__(self) -> 'TemporaryDirectory': 165 | """Create the temporary directory.""" 166 | if self.exited: 167 | raise RuntimeError("Already exited") 168 | 169 | base_tmp_dir = str(self.base_tmp_dir) if self.base_tmp_dir is not None else None 170 | self._path = pathlib.Path(tempfile.mkdtemp(prefix=self.prefix, dir=base_tmp_dir)) 171 | 172 | return self 173 | 174 | @property 175 | def path(self) -> pathlib.Path: 176 | """Get the underlying path or raise if the path has not been set.""" 177 | if self._path is None: 178 | raise RuntimeError("The _path has not been set. " 179 | f"Are you using {self.__class__.__name__} outside of the context management?") 180 | 181 | return self._path 182 | 183 | def close(self) -> None: 184 | """ 185 | Close the temporary directory. 186 | 187 | If already closed, does nothing. If dont_delete not set, deletes the temporary directory if it exists. 188 | 189 | """ 190 | if not self.exited: 191 | if not self.dont_delete and self._path is not None and self._path.exists(): 192 | shutil.rmtree(str(self._path)) 193 | 194 | self.exited = True 195 | 196 | def __exit__(self, exc_type, exc_val, exc_tb): # type: ignore 197 | """Close the temporary directory upon exit.""" 198 | self.close() 199 | 200 | 201 | class NamedTemporaryFile: 202 | """Wrap tempfile.NamedTemporaryFile with pathlib.Path.""" 203 | 204 | def __init__( 205 | self, 206 | mode: str = 'w+b', 207 | buffering: int = -1, 208 | encoding: Optional[str] = None, 209 | newline: Optional[str] = None, 210 | suffix: Optional[str] = None, 211 | prefix: Optional[str] = None, 212 | dir: Optional[pathlib.Path] = None, # pylint: disable=redefined-builtin 213 | delete: bool = True) -> None: 214 | """ 215 | Initialize with the given values. 216 | 217 | The description of parameters is copied from the tempfile.NamedTemporaryFile docstring. 218 | 219 | :param mode: the mode argument to io.open (default "w+b") 220 | :param buffering: the buffer size argument to io.open (default -1). 221 | :param encoding: the encoding argument to io.open (default None) 222 | :param newline: the newline argument to io.open (default None) 223 | :param suffix: If 'suffix' is not None, the file name will end with that suffix, 224 | otherwise there will be no suffix. 225 | 226 | :param prefix: If 'prefix' is not None, the file name will begin with that prefix, 227 | otherwise a default prefix is used. 228 | 229 | :param dir: If 'dir' is not None, the file will be created in that directory, 230 | otherwise a default directory is used. 231 | 232 | :param delete: whether the file is deleted on close (default True). 233 | """ 234 | # pylint: disable=consider-using-with 235 | self.__tmpfile = tempfile.NamedTemporaryFile( 236 | mode=mode, 237 | buffering=buffering, 238 | encoding=encoding, 239 | newline=newline, 240 | suffix=suffix, 241 | prefix=prefix, 242 | dir=str(dir) if dir is not None else None, 243 | delete=delete) 244 | 245 | self.path = pathlib.Path(self.__tmpfile.name) 246 | 247 | file = self.__tmpfile.file 248 | self.file = file # type: IO[Any] 249 | self.delete = delete 250 | 251 | def close(self) -> None: 252 | """Forward close request to the underlying temporary file.""" 253 | self.__tmpfile.close() 254 | 255 | def __enter__(self) -> 'NamedTemporaryFile': 256 | """Return this object; no further action is performed.""" 257 | return self 258 | 259 | def __exit__(self, exc_type, exc_val, exc_tb) -> None: # type: ignore 260 | """Close the temporary file.""" 261 | self.close() 262 | 263 | 264 | def gettempdir() -> pathlib.Path: 265 | """ 266 | Wrap ``tempfile.gettempdir``. 267 | 268 | Please see the documentation of ``tempfile.gettempdir`` for more details: 269 | https://docs.python.org/3/library/tempfile.html#tempfile.gettempdir 270 | """ 271 | return pathlib.Path(tempfile.gettempdir()) 272 | -------------------------------------------------------------------------------- /temppathlib/py.typed: -------------------------------------------------------------------------------- 1 | # Marker file for PEP 561. The mypy package uses inline types. 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Test temppathlib.""" 2 | -------------------------------------------------------------------------------- /tests/test_temppathlib.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # pylint: disable=missing-docstring,too-many-public-methods 4 | 5 | import copy 6 | import os 7 | import pathlib 8 | import shutil 9 | import tempfile 10 | import unittest 11 | from typing import Optional # pylint: disable=unused-import 12 | 13 | import temppathlib 14 | 15 | 16 | class TestRemovingTree(unittest.TestCase): 17 | def test_that_it_works(self) -> None: 18 | tmp_dir = pathlib.Path(tempfile.mkdtemp()) 19 | 20 | try: 21 | pth = tmp_dir / 'oioi' 22 | pth.mkdir() 23 | with temppathlib.removing_tree(path=pth): 24 | self.assertTrue(pth.exists()) 25 | 26 | subpth = pth / 'oi.txt' 27 | with subpth.open('wt', encoding='utf-8') as fid: 28 | fid.write('hey!') 29 | fid.flush() 30 | 31 | self.assertTrue(subpth.exists()) 32 | 33 | self.assertFalse(pth.exists()) 34 | 35 | finally: 36 | if tmp_dir.exists(): 37 | shutil.rmtree(str(tmp_dir)) 38 | 39 | def test_no_enter(self) -> None: 40 | tmp_dir = pathlib.Path(tempfile.mkdtemp()) 41 | 42 | try: 43 | dir1 = tmp_dir / "dir1" 44 | dir1.mkdir() 45 | dir2 = tmp_dir / "dir2" 46 | dir2.mkdir() 47 | 48 | names = sorted(pth.name for pth in tmp_dir.iterdir()) 49 | self.assertListEqual(names, ['dir1', 'dir2']) 50 | 51 | # context manager invoked without enter does not delete the path. 52 | temppathlib.removing_tree(path=dir1) 53 | 54 | names = sorted(pth.name for pth in tmp_dir.iterdir()) 55 | self.assertListEqual(names, ['dir1', 'dir2']) 56 | 57 | # context manager invoked with enter does delete the path. 58 | with temppathlib.removing_tree(path=dir1): 59 | pass 60 | 61 | names = sorted(pth.name for pth in tmp_dir.iterdir()) 62 | self.assertListEqual(names, ['dir2']) 63 | finally: 64 | if tmp_dir.exists(): 65 | shutil.rmtree(str(tmp_dir)) 66 | 67 | 68 | class TestTmpDirIfNecessary(unittest.TestCase): 69 | def test_with_path_str(self) -> None: 70 | basedir = pathlib.Path(tempfile.mkdtemp()) 71 | 72 | try: 73 | notmp_pth = str(basedir / "no-tmp") 74 | with temppathlib.TmpDirIfNecessary(path=notmp_pth) as maybe_tmp_dir: 75 | self.assertEqual(pathlib.Path(notmp_pth), maybe_tmp_dir.path) 76 | 77 | self.assertTrue(os.path.exists(notmp_pth)) 78 | 79 | finally: 80 | shutil.rmtree(str(basedir)) 81 | 82 | def test_with_base_tmp_dir(self) -> None: 83 | basedir = pathlib.Path(tempfile.mkdtemp()) 84 | 85 | try: 86 | tmp_pth = None # type: Optional[pathlib.Path] 87 | with temppathlib.TmpDirIfNecessary(path=None, base_tmp_dir=basedir) as maybe_tmp_dir: 88 | tmp_pth = maybe_tmp_dir.path 89 | 90 | self.assertTrue(tmp_pth.parent == basedir) 91 | 92 | self.assertFalse(tmp_pth.exists()) 93 | 94 | finally: 95 | shutil.rmtree(str(basedir)) 96 | 97 | def test_prefix(self) -> None: 98 | with temppathlib.TmpDirIfNecessary(path=None, prefix="some_prefix") as tmp_dir: 99 | self.assertTrue(tmp_dir.path.name.startswith("some_prefix")) 100 | 101 | def test_suffix(self) -> None: 102 | with temppathlib.TmpDirIfNecessary(path=None, suffix="some_suffix") as tmp_dir: 103 | self.assertTrue(tmp_dir.path.name.endswith("some_suffix")) 104 | 105 | 106 | class TestTemporaryDirectory(unittest.TestCase): 107 | def test_that_it_works(self) -> None: 108 | tmp_dir = pathlib.Path(tempfile.mkdtemp()) 109 | 110 | try: 111 | another_tmp_dir_pth = None # type: Optional[pathlib.Path] 112 | with temppathlib.TemporaryDirectory(base_tmp_dir=tmp_dir) as another_tmp_dir: 113 | another_tmp_dir_pth = copy.copy(another_tmp_dir.path) 114 | 115 | self.assertTrue(another_tmp_dir_pth.exists()) 116 | 117 | self.assertFalse(another_tmp_dir_pth.exists()) 118 | 119 | finally: 120 | if tmp_dir.exists(): 121 | shutil.rmtree(str(tmp_dir)) 122 | 123 | def test_with_prefix(self) -> None: 124 | with temppathlib.TemporaryDirectory(prefix='some-prefix') as tmp_dir: 125 | self.assertTrue(tmp_dir.path.name.startswith('some-prefix')) 126 | 127 | 128 | class TestNamedTemporaryFile(unittest.TestCase): 129 | def test_that_it_works(self) -> None: 130 | pth = None # type: Optional[pathlib.Path] 131 | with temppathlib.NamedTemporaryFile() as tmp: 132 | self.assertIsNotNone(tmp.file) 133 | self.assertTrue(tmp.path.exists()) 134 | 135 | pth = tmp.path 136 | 137 | self.assertFalse(pth.exists()) 138 | 139 | def test_with_dir(self) -> None: 140 | with temppathlib.TemporaryDirectory() as tmp_dir: 141 | with temppathlib.NamedTemporaryFile(dir=tmp_dir.path) as tmp: 142 | self.assertIsNotNone(tmp.file) 143 | self.assertTrue(tmp.path.exists()) 144 | 145 | 146 | class TestGettempdir(unittest.TestCase): 147 | def test_that_it_works(self) -> None: 148 | tmpdir = temppathlib.gettempdir() 149 | 150 | original_tmpdir = tempfile.gettempdir() 151 | self.assertEqual(original_tmpdir, str(tmpdir)) 152 | 153 | 154 | if __name__ == '__main__': 155 | unittest.main() 156 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py35,py36,py37,py38 3 | 4 | [testenv] 5 | deps = .[dev] 6 | changedir = {envtmpdir} 7 | commands = 8 | python3 {toxinidir}/precommit.py 9 | 10 | setenv = 11 | COVERAGE_FILE={envbindir}/.coverage 12 | --------------------------------------------------------------------------------