diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..75d95dd --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,17 @@ +--- +engines: + duplication: + enabled: true + config: + languages: + - python + exclude_fingerprints: + - a455ac0cedfef808dec73ada52eeea77 + fixme: + enabled: true + radon: + enabled: true +ratings: + paths: + - "**.py" +exclude_paths: [] diff --git a/.dockerignore b/.dockerignore index acfde92..4da5a92 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,91 @@ +# Git .git .gitignore -.landscape.yml -.ropeproject + +# CI +.codeclimate.yml .travis.yml -fig.yml + +# Docker +docker-compose.yml + +# Byte-compiled / optimized / DLL files +__pycache__/ +*/__pycache__/ +*/*/__pycache__/ +*/*/*/__pycache__/ +*.py[cod] +*/*.py[cod] +*/*/*.py[cod] +*/*/*/*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Virtual environment +.env/ +.venv/ +venv/ + +# PyCharm +.idea + +# Python mode for VIM +.ropeproject +*/.ropeproject +*/*/.ropeproject +*/*/*/.ropeproject + +# Vim swap files *.swp +*/*.swp +*/*/*.swp +*/*/*/*.swp diff --git a/.gitignore b/.gitignore index 1957176..269fbdf 100644 --- a/.gitignore +++ b/.gitignore @@ -54,7 +54,9 @@ docs/_build/ target/ # Virtual environment -.env +.env/ +.venv/ +venv/ # PyCharm .idea diff --git a/.landscape.yaml b/.landscape.yaml deleted file mode 100644 index 567b633..0000000 --- a/.landscape.yaml +++ /dev/null @@ -1,5 +0,0 @@ -doc-warnings: no -test-warnings: yes -strictness: medium -max-line-length: 80 -autodetect: yes diff --git a/.pylintrc b/.pylintrc index ea1da8c..b6d8b4c 100644 --- a/.pylintrc +++ b/.pylintrc @@ -7,9 +7,6 @@ # pygtk.require(). #init-hook= -# Profiled execution. -profile=no - # Add files or directories to the blacklist. They should be base names, not # paths. ignore=CVS @@ -21,9 +18,33 @@ persistent=yes # usually to register additional checkers. load-plugins= +# Use multiple processes to speed up Pylint. +jobs=1 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Allow optimization of some AST trees. This will activate a peephole AST +# optimizer, which will apply various small optimizations. For instance, it can +# be used to obtain the result of joining multiple strings with the addition +# operator. Joining a lot of strings can lead to a maximum recursion error in +# Pylint and this flag can prevent that. It has one side effect, the resulting +# AST will be different than the one from reality. +optimize-ast=no + [MESSAGES CONTROL] +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time. See also the "--disable" option for examples. @@ -38,9 +59,7 @@ load-plugins= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" - -# C0111 missing-docstring -disable=C0111 +disable=suppressed-message,dict-iter-method,getslice-method,range-builtin-not-iterating,useless-suppression,input-builtin,indexing-exception,reload-builtin,import-star-module-level,nonzero-method,print-statement,cmp-builtin,oct-method,metaclass-assignment,xrange-builtin,long-builtin,old-octal-literal,coerce-method,raising-string,basestring-builtin,old-division,old-ne-operator,round-builtin,old-raise-syntax,coerce-builtin,execfile-builtin,dict-view-method,raw_input-builtin,unichr-builtin,no-absolute-import,using-cmp-argument,hex-method,unicode-builtin,next-method-called,delslice-method,unpacking-in-except,standarderror-builtin,cmp-method,intern-builtin,backtick,reduce-builtin,map-builtin-not-iterating,apply-builtin,buffer-builtin,file-builtin,zip-builtin-not-iterating,filter-builtin-not-iterating,long-suffix,parameter-unpacking,setslice-method,missing-docstring,not-context-manager [REPORTS] @@ -65,112 +84,195 @@ reports=yes # (RP0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) -# Add a comment according to your evaluation note. This is used by the global -# evaluation report (RP0004). -comment=no - # Template used to display messages. This is a python new-style format string # used to format the message information. See doc for all details #msg-template= -[SIMILARITIES] +[SPELLING] -# Minimum lines number of a similarity. -min-similarity-lines=4 +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= -# Ignore comments when computing similarities. -ignore-comments=yes +# List of comma separated words that should not be checked. +spelling-ignore-words= -# Ignore docstrings when computing similarities. -ignore-docstrings=yes +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= -# Ignore imports when computing similarities. -ignore-imports=no +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no -[BASIC] +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=120 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma,dict-separator -# Required attributes for module, separated by a comma -required-attributes= +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# List of classes names for which member attributes should not be checked +# (useful for classes with attributes dynamically set). This supports can work +# with qualified names. +ignored-classes= + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + + +[BASIC] # List of builtins function names that should not be used, separated by a comma -bad-functions=map,filter,apply,input +bad-functions=map,filter -# Regular expression which should only match correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ +# Good variable names which should always be accepted, separated by a comma +good-names=d,e,f,i,j,k,ex,fn,fd,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# Regular expression matching correct method names +method-rgx=[a-z_][a-z0-9_]{2,70}$ + +# Naming hint for method names +method-name-hint=[a-z_][a-z0-9_]{2,70}$ -# Regular expression which should only match correct module level names +# Regular expression matching correct constant names const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ -# Regular expression which should only match correct class names -class-rgx=[A-Z_][a-zA-Z0-9]+$ +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,70}$ -# Regular expression which should only match correct function names +# Naming hint for argument names +argument-name-hint=[a-z_][a-z0-9_]{2,70}$ + +# Regular expression matching correct function names function-rgx=[a-z_][a-z0-9_]{2,70}$ -# Regular expression which should only match correct method names -method-rgx=[a-z_][a-z0-9_]{2,30}$ +# Naming hint for function names +function-name-hint=[a-z_][a-z0-9_]{2,70}$ -# Regular expression which should only match correct instance attribute names -attr-rgx=[a-z_][a-z0-9_]{2,30}$ +# Regular expression matching correct variable names +variable-rgx=[a-z_][a-z0-9_]{1,70}$ -# Regular expression which should only match correct argument names -argument-rgx=[a-z_][a-z0-9_]{2,30}$ +# Naming hint for variable names +variable-name-hint=[a-z_][a-z0-9_]{1,70}$ -# Regular expression which should only match correct variable names -variable-rgx=[a-z_][a-z0-9_]{0,30}$ +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ -# Regular expression which should only match correct attribute names in class -# bodies -class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ +# Naming hint for class names +class-name-hint=[A-Z_][a-zA-Z0-9]+$ -# Regular expression which should only match correct list comprehension / -# generator expression variable names +# Regular expression matching correct attribute names +attr-rgx=[a-z_][a-z0-9_]{1,30}$ + +# Naming hint for attribute names +attr-name-hint=[a-z_][a-z0-9_]{1,30}$ + +# Regular expression matching correct inline iteration names inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ -# Good variable names which should always be accepted, separated by a comma -good-names=i,j,k,ex,Run,_ +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ -# Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{1,30}|(__.*__))$ + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{1,30}|(__.*__))$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Regular expression which should only match function or class names that do # not require a docstring. -no-docstring-rgx=__.*__ +no-docstring-rgx=^_ # Minimum line length for functions/classes that require docstrings, shorter # ones are exempt. docstring-min-length=-1 -[FORMAT] +[ELIF] -# Maximum number of characters on a single line. -max-line-length=80 +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - -# List of optional constructs for which whitespace checking is disabled -no-space-check=trailing-comma,dict-separator - -# Maximum number of lines in a module -max-module-lines=1000 +[SIMILARITIES] -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' +# Minimum lines number of a similarity. +min-similarity-lines=4 +# Ignore comments when computing similarities. +ignore-comments=yes -[MISCELLANEOUS] +# Ignore docstrings when computing similarities. +ignore-docstrings=yes -# List of note tags to take in consideration, separated by a comma. -notes=FIXME,XXX,TODO +# Ignore imports when computing similarities. +ignore-imports=no [VARIABLES] @@ -178,39 +280,30 @@ notes=FIXME,XXX,TODO # Tells whether we should check for unused import in __init__ files. init-import=no -# A regular expression matching the beginning of the name of dummy variables -# (i.e. not used). +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). dummy-variables-rgx=_$|dummy # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. additional-builtins= +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb -[TYPECHECK] - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# List of classes names for which member attributes should not be checked -# (useful for classes with attributes dynamically set). -ignored-classes=SQLObject -# When zope mode is activated, add a predefined set of Zope acquired attributes -# to generated-members. -zope=no +[LOGGING] -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E0201 when accessed. Python regular -# expressions are accepted. -generated-members=REQUEST,acl_users,aq_parent +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging [IMPORTS] # Deprecated modules which should not be used, separated by a comma -deprecated-modules=regsub,TERMIOS,Bastion,rexec +deprecated-modules=optparse # Create a graph of every (i.e. internal and external) dependencies in the # given file (report RP0402 must not be disabled) @@ -227,10 +320,6 @@ int-import-graph= [CLASSES] -# List of interface methods to ignore, separated by a comma. This is used for -# instance to not check methods defines in Zope's Interface base class. -ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by - # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__,__new__,setUp @@ -240,6 +329,10 @@ valid-classmethod-first-arg=cls # List of valid names for the first argument in a metaclass class method. valid-metaclass-classmethod-first-arg=mcs +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + [DESIGN] @@ -274,6 +367,9 @@ min-public-methods=2 # Maximum number of public methods for a class (see R0904). max-public-methods=20 +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + [EXCEPTIONS] diff --git a/.travis.yml b/.travis.yml index 25e6827..4e4d5a3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,30 +1,39 @@ -sudo: false language: python -python: -- 2.6 -- 2.7 -- 3.2 -- 3.3 -- 3.4 -- pypy -- pypy3 +matrix: + include: + - python: 2.6 + env: + - TOXENV=py26 + - python: 2.7 + env: + - TOXENV=py27 + - python: 3.3 + env: + - TOXENV=py33 + - python: 3.4 + env: + - TOXENV=py34 + - python: 3.5 + env: + - TOXENV=py35 + - python: pypy + env: + - TOXENV=pypy + - python: pypy3 + env: + - TOXENV=pypy3 install: -- pip install -r requirements_test_runner.txt -- pip install -r requirements_static_analysis.txt -- pip install -r requirements_test.txt -- pip install coveralls -- python setup.py develop -before_script: -- flake8 setup.py "temporary" -- pyflakes setup.py "temporary" -script: nosetests --with-doctest --with-coverage --cover-tests --cover-inclusive --cover-package="temporary" "temporary" -after_success: coveralls + - pip install tox coveralls +script: + - tox +after_success: + - coveralls deploy: provider: pypi user: themattrix distributions: sdist bdist_wheel on: - python: 2.7 + condition: $TOXENV == py27 tags: true all_branches: true repo: themattrix/python-temporary diff --git a/Dockerfile b/Dockerfile index 6c54c3f..ced9105 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,40 +1,3 @@ -FROM ubuntu:14.04 +FROM themattrix/tox MAINTAINER Matthew Tardiff - -ENV DEBIAN_FRONTEND noninteractive - -RUN apt-get update \ - && apt-get -y install \ - python-software-properties software-properties-common \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* - -RUN add-apt-repository -y ppa:fkrull/deadsnakes - -RUN apt-get update \ - && apt-get -y install \ - wget python-pip \ - python2.6 python2.7 python3.2 python3.3 python3.4 \ - pypy \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* - -RUN mkdir /install -RUN wget -O /install/pypy3-2.4.0-linux64.tar.bz2 \ - https://bitbucket.org/pypy/pypy/downloads/pypy3-2.4.0-linux64.tar.bz2 - -RUN tar jxf /install/pypy3-2.4.0-linux64.tar.bz2 -C /install -RUN ln -s /install/pypy3-2.4.0-linux64/bin/pypy3 /usr/local/bin/pypy3 - -RUN pip install tox - -RUN mkdir /app -WORKDIR /app -ADD requirements*.txt /app/ -ADD tox.ini /app/tox.ini -RUN TOXSKIPSDIST=true TOXCOMMANDS=installonly tox - -ADD . /app/ - -CMD ["tox"] diff --git a/LICENSE b/LICENSE index 9d718e0..fc79fec 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Matthew Tardiff +Copyright (c) 2015-2016 Matthew Tardiff Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.rst b/README.rst index bb8e271..9798ee2 100644 --- a/README.rst +++ b/README.rst @@ -8,10 +8,10 @@ Context managers for managing temporary files and directories. .. code:: python - with temp_dir() as d: + with temporary.temp_dir() as d: ... - with temp_file(content='hello') as f: + with temporary.temp_file(content='hello') as f: ... @@ -25,76 +25,69 @@ Installation: Temporary Directory Examples ---------------------------- -.. code:: python - - >>> from temporary import temp_dir, in_temp_dir - The temporary directory is created when entering the context manager, and deleted when exiting it: .. code:: python - >>> from os.path import exists, isdir - >>> with temp_dir() as d: - ... assert isdir(d) - >>> assert not exists(d) + >>> import os.path + >>> import temporary + >>> with temporary.temp_dir() as d: + ... assert os.path.isdir(d) + >>> assert not os.path.exists(d) This time let's make the temporary directory our working directory: .. code:: python - >>> from os import getcwd - >>> with temp_dir(make_cwd=True) as d: - ... assert d == getcwd() - >>> assert d != getcwd() + >>> import os + >>> with temporary.temp_dir(make_cwd=True) as d: + ... assert d == os.getcwd() + >>> assert d != os.getcwd() The suffix, prefix, and parent_dir options are passed to the standard ``tempfile.mkdtemp()`` function: .. code:: python - >>> from os.path import basename, dirname - >>> with temp_dir() as p: - ... with temp_dir(suffix='suf', prefix='pre', parent_dir=p) as d: - ... assert dirname(d) == p - ... assert basename(d).startswith('pre') - ... assert basename(d).endswith('suf') + >>> with temporary.temp_dir() as p: + ... with temporary.temp_dir(suffix='suf', prefix='pre', parent_dir=p) as d: + ... assert os.path.dirname(d) == p + ... assert os.path.basename(d).startswith('pre') + ... assert os.path.basename(d).endswith('suf') This function can also be used as a decorator, with the ``in_temp_dir`` alias: .. code:: python - >>> @in_temp_dir() + >>> @temporary.in_temp_dir() ... def my_function(): - ... assert old_cwd != getcwd() + ... assert old_cwd != os.getcwd() ... - >>> old_cwd = getcwd() + >>> old_cwd = os.getcwd() >>> my_function() - >>> assert old_cwd == getcwd() + >>> assert old_cwd == os.getcwd() Temporary Files Examples ------------------------ -.. code:: python - - >>> from temporary import temp_file - The temporary file is created when entering the context manager and deleted when exiting it. .. code:: python - >>> from os.path import exists, isfile - >>> with temp_file() as f_name: - ... assert isfile(f_name) - >>> assert not exists(f_name) + >>> import os.path + >>> import temporary + >>> with temporary.temp_file() as f_name: + ... assert os.path.isfile(f_name) + >>> assert not os.path.exists(f_name) The user may also supply some content for the file to be populated with: .. code:: python - >>> with temp_file('hello!') as f_name: + >>> with temporary.temp_file('hello!') as f_name: ... with open(f_name) as f: ... assert f.read() == 'hello!' @@ -102,11 +95,9 @@ The temporary file can be placed in a custom directory: .. code:: python - >>> from os.path import dirname - >>> from temporary import temp_dir - >>> with temp_dir() as d_name: - ... with temp_file(parent_dir=d_name) as f_name: - ... assert dirname(f_name) == d_name + >>> with temporary.temp_dir() as d_name: + ... with temporary.temp_file(parent_dir=d_name) as f_name: + ... assert os.path.dirname(f_name) == d_name If, for some reason, the user wants to delete the temporary file before exiting the context, that's okay too: @@ -114,7 +105,7 @@ exiting the context, that's okay too: .. code:: python >>> import os - >>> with temp_file() as f_name: + >>> with temporary.temp_file() as f_name: ... os.remove(f_name) @@ -122,15 +113,15 @@ exiting the context, that's okay too: :target: https://travis-ci.org/themattrix/python-temporary .. |Coverage| image:: https://img.shields.io/coveralls/themattrix/python-temporary.svg :target: https://coveralls.io/r/themattrix/python-temporary -.. |Health| image:: https://landscape.io/github/themattrix/python-temporary/master/landscape.svg - :target: https://landscape.io/github/themattrix/python-temporary/master -.. |Version| image:: https://pypip.in/version/temporary/badge.svg?text=version - :target: https://pypi.python.org/pypi/temporary -.. |Downloads| image:: https://pypip.in/download/temporary/badge.svg - :target: https://pypi.python.org/pypi/temporary -.. |Compatibility| image:: https://pypip.in/py_versions/temporary/badge.svg - :target: https://pypi.python.org/pypi/temporary -.. |Implementations| image:: https://pypip.in/implementation/temporary/badge.svg - :target: https://pypi.python.org/pypi/temporary -.. |Format| image:: https://pypip.in/format/temporary/badge.svg - :target: https://pypi.python.org/pypi/temporary +.. |Health| image:: https://codeclimate.com/github/themattrix/python-temporary/badges/gpa.svg + :target: https://codeclimate.com/github/themattrix/python-temporary +.. |Version| image:: https://img.shields.io/pypi/v/temporary.svg + :target: https://pypi.python.org/pypi/temporary +.. |Downloads| image:: https://img.shields.io/pypi/dm/temporary.svg + :target: https://pypi.python.org/pypi/temporary +.. |Compatibility| image:: https://img.shields.io/pypi/pyversions/temporary.svg + :target: https://pypi.python.org/pypi/temporary +.. |Implementations| image:: https://img.shields.io/pypi/implementation/temporary.svg + :target: https://pypi.python.org/pypi/temporary +.. |Format| image:: https://img.shields.io/pypi/format/temporary.svg + :target: https://pypi.python.org/pypi/temporary diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ddd84c9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,4 @@ +tox: + build: . + volumes: + - ".:/src:ro" diff --git a/fig.yml b/fig.yml deleted file mode 100644 index 6bee5fa..0000000 --- a/fig.yml +++ /dev/null @@ -1,2 +0,0 @@ -tox: - build: . diff --git a/requirements_static_analysis.txt b/requirements_static_analysis.txt index 33c3fdf..0841b08 100644 --- a/requirements_static_analysis.txt +++ b/requirements_static_analysis.txt @@ -1,3 +1,7 @@ +# Docs analysis, same versions used by PyPI +docutils==0.12 +pygments==2.0.2 + +# Code analysis flake8 -pyflakes pylint diff --git a/setup.py b/setup.py index 2b32580..150b678 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='temporary', - version='1.0.1', + version='1.1.0', packages=('temporary',), url='https://github.com/themattrix/python-temporary', license='MIT', @@ -24,5 +24,6 @@ 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy')) diff --git a/temporary/__init__.py b/temporary/__init__.py index 206cd1c..ad23b60 100644 --- a/temporary/__init__.py +++ b/temporary/__init__.py @@ -1,7 +1,7 @@ -from temporary.directories import temp_dir, in_temp_dir -from temporary.files import temp_file +# pylint: disable=invalid-name +import temporary.directories +import temporary.files -# silence PyFlakes -assert temp_dir -assert in_temp_dir -assert temp_file +temp_file = temporary.files.temp_file +temp_dir = temporary.directories.temp_dir +in_temp_dir = temporary.directories.in_temp_dir diff --git a/temporary/directories.py b/temporary/directories.py index 658f9c2..27931a9 100644 --- a/temporary/directories.py +++ b/temporary/directories.py @@ -1,62 +1,63 @@ -from contextlib2 import contextmanager -from errno import ENOENT -from functools import partial -from os import chdir, getcwd -from shutil import rmtree -from tempfile import mkdtemp +import functools +import os +import shutil +import tempfile +import contextlib2 as contextlib -@contextmanager +import temporary.util + + +@contextlib.contextmanager def temp_dir(suffix='', prefix='tmp', parent_dir=None, make_cwd=False): """ - Create a temporary directory and optionally change the current working - directory to it. The directory is deleted when the context exits. - - The temporary directory is created when entering the context manager, and - deleted when exiting it: - >>> from os.path import exists, isdir - >>> with temp_dir() as d: - ... assert isdir(d) - >>> assert not exists(d) + Create a temporary directory and optionally change the current + working directory to it. The directory is deleted when the context + exits. + + The temporary directory is created when entering the context + manager, and deleted when exiting it: + >>> import os.path + >>> import temporary + >>> with temporary.temp_dir() as d: + ... assert os.path.isdir(d) + >>> assert not os.path.exists(d) This time let's make the temporary directory our working directory: - >>> from os import getcwd - >>> with temp_dir(make_cwd=True) as d: - ... assert d == getcwd() - >>> assert d != getcwd() - - The suffix, prefix, and parent_dir options are passed to the standard - tempfile.mkdtemp() function: - >>> from os.path import basename, dirname - >>> with temp_dir() as p: - ... with temp_dir(suffix='suf', prefix='pre', parent_dir=p) as d: - ... assert dirname(d) == p - ... assert basename(d).startswith('pre') - ... assert basename(d).endswith('suf') - - This function can also be used as a decorator, with the in_temp_dir alias: - >>> @in_temp_dir() + >>> import os + >>> with temporary.temp_dir(make_cwd=True) as d: + ... assert d == os.getcwd() + >>> assert d != os.getcwd() + + The suffix, prefix, and parent_dir options are passed to the + standard ``tempfile.mkdtemp()`` function: + >>> with temporary.temp_dir() as p: + ... with temporary.temp_dir(suffix='suf', prefix='pre', parent_dir=p) as d: + ... assert os.path.dirname(d) == p + ... assert os.path.basename(d).startswith('pre') + ... assert os.path.basename(d).endswith('suf') + + This function can also be used as a decorator, with the in_temp_dir + alias: + >>> @temporary.in_temp_dir() ... def my_function(): - ... assert old_cwd != getcwd() + ... assert old_cwd != os.getcwd() ... - >>> old_cwd = getcwd() + >>> old_cwd = os.getcwd() >>> my_function() - >>> assert old_cwd == getcwd() + >>> assert old_cwd == os.getcwd() """ - prev_cwd = getcwd() - abs_path = mkdtemp(suffix, prefix, parent_dir) + prev_cwd = os.getcwd() + abs_path = tempfile.mkdtemp(suffix, prefix, parent_dir) try: if make_cwd: - chdir(abs_path) + os.chdir(abs_path) yield abs_path finally: if make_cwd: - chdir(prev_cwd) - try: - rmtree(abs_path) - except OSError as e: - if e.errno != ENOENT: - raise + os.chdir(prev_cwd) + with temporary.util.allow_missing_file(): + shutil.rmtree(abs_path) -in_temp_dir = partial(temp_dir, make_cwd=True) # pylint: disable=invalid-name +in_temp_dir = functools.partial(temp_dir, make_cwd=True) # pylint: disable=invalid-name diff --git a/temporary/files.py b/temporary/files.py index 5e76d9b..3d11646 100644 --- a/temporary/files.py +++ b/temporary/files.py @@ -1,10 +1,12 @@ -from errno import ENOENT -from contextlib import contextmanager -from os import close, remove, write -from tempfile import mkstemp +import os +import tempfile +import contextlib2 as contextlib -@contextmanager +import temporary.util + + +@contextlib.contextmanager def temp_file( content=None, suffix='', @@ -17,39 +19,37 @@ def temp_file( The temporary file is created when entering the context manager and deleted when exiting it. - >>> from os.path import exists, isfile - >>> with temp_file() as f_name: - ... assert isfile(f_name) - >>> assert not exists(f_name) + >>> import os.path + >>> import temporary + >>> with temporary.temp_file() as f_name: + ... assert os.path.isfile(f_name) + >>> assert not os.path.exists(f_name) The user may also supply some content for the file to be populated with: - >>> with temp_file('hello!') as f_name: + >>> with temporary.temp_file('hello!') as f_name: ... with open(f_name) as f: ... assert f.read() == 'hello!' The temporary file can be placed in a custom directory: - >>> from os.path import dirname - >>> from temporary import temp_dir - >>> with temp_dir() as d_name: - ... with temp_file(parent_dir=d_name) as f_name: - ... assert dirname(f_name) == d_name + >>> with temporary.temp_dir() as d_name: + ... with temporary.temp_file(parent_dir=d_name) as f_name: + ... assert os.path.dirname(f_name) == d_name If, for some reason, the user wants to delete the temporary file before exiting the context, that's okay too: >>> import os - >>> with temp_file() as f_name: + >>> with temporary.temp_file() as f_name: ... os.remove(f_name) """ - fd, abs_path = mkstemp(suffix, prefix, parent_dir, not binary) + fd, abs_path = tempfile.mkstemp(suffix, prefix, parent_dir, not binary) try: - if content: - write(fd, content.encode()) - close(fd) + try: + if content: + os.write(fd, content.encode()) + finally: + os.close(fd) yield abs_path finally: - try: - remove(abs_path) - except OSError as e: - if e.errno != ENOENT: - raise + with temporary.util.allow_missing_file(): + os.remove(abs_path) diff --git a/temporary/test/example_1/example.py b/temporary/test/example_1/example.py index d6cf9f4..1517146 100644 --- a/temporary/test/example_1/example.py +++ b/temporary/test/example_1/example.py @@ -1,9 +1,10 @@ -from os import getcwd -from os.path import exists -from temporary import temp_dir +import os +import os.path -with temp_dir(make_cwd=True) as d: - assert d == getcwd() +import temporary -assert not exists(d) -assert d != getcwd() +with temporary.temp_dir(make_cwd=True) as d: + assert d == os.getcwd() + +assert not os.path.exists(d) +assert d != os.getcwd() diff --git a/temporary/test/test_directories.py b/temporary/test/test_directories.py index d031cfb..5ff4d07 100644 --- a/temporary/test/test_directories.py +++ b/temporary/test/test_directories.py @@ -1,25 +1,25 @@ -from contextlib2 import contextmanager -from errno import EEXIST, ENOENT -from nose.tools import eq_, raises -from os import chdir, getcwd, makedirs, rmdir -from os.path import dirname, exists, isdir, isfile, join -from simian import patch +import errno +import os +import os.path -# module under test -from temporary import directories +import contextlib2 as contextlib +import nose.tools +import simian + +import temporary # # Decorators # -@contextmanager +@contextlib.contextmanager def restore_cwd(): - cwd = getcwd() + cwd = os.getcwd() try: yield finally: - chdir(cwd) + os.chdir(cwd) # @@ -34,68 +34,74 @@ class DummyException(Exception): # Tests # +# noinspection PyCallingNonCallable @restore_cwd() def test_temp_dir_without_chdir_creates_temp_dir(): - cwd = getcwd() - with directories.temp_dir() as d: - eq_(isdir(d), True) - eq_(getcwd(), cwd) - eq_(exists(d), False) - eq_(getcwd(), cwd) + cwd = os.getcwd() + with temporary.temp_dir() as d: + nose.tools.eq_(os.path.isdir(d), True) + nose.tools.eq_(os.getcwd(), cwd) + nose.tools.eq_(os.path.exists(d), False) + nose.tools.eq_(os.getcwd(), cwd) +# noinspection PyCallingNonCallable @restore_cwd() def test_temp_dir_with_chdir_creates_temp_dir(): - cwd = getcwd() - with directories.temp_dir(make_cwd=True) as d: - eq_(isdir(d), True) - eq_(getcwd(), d) - eq_(exists(d), False) - eq_(getcwd(), cwd) + cwd = os.getcwd() + with temporary.temp_dir(make_cwd=True) as d: + nose.tools.eq_(os.path.isdir(d), True) + nose.tools.eq_(os.getcwd(), d) + nose.tools.eq_(os.path.exists(d), False) + nose.tools.eq_(os.getcwd(), cwd) +# noinspection PyCallingNonCallable @restore_cwd() def test_temp_dir_deletes_all_children(): - with directories.temp_dir() as d: - f = join(d, 'deep', 'deeper', 'file') + with temporary.temp_dir() as d: + f = os.path.join(d, 'deep', 'deeper', 'file') create_file_in_tree(f) - eq_(isfile(f), True) - eq_(exists(d), False) - eq_(exists(f), False) + nose.tools.eq_(os.path.isfile(f), True) + nose.tools.eq_(os.path.exists(d), False) + nose.tools.eq_(os.path.exists(f), False) +# noinspection PyCallingNonCallable @restore_cwd() def test_changing_to_temp_dir_manually_still_allows_deletion(): - with directories.temp_dir() as d: - chdir(d) + with temporary.temp_dir() as d: + os.chdir(d) +# noinspection PyCallingNonCallable @restore_cwd() def test_manually_deleting_temp_dir_is_allowed(): - with directories.temp_dir() as d: - rmdir(d) + with temporary.temp_dir() as d: + os.rmdir(d) -@raises(OSError) -@patch(directories, external=('shutil.rmtree',)) +# noinspection PyCallingNonCallable +@nose.tools.raises(OSError) +@simian.patch(temporary.directories, external=('shutil.rmtree',)) @restore_cwd() def test_temp_dir_with_failed_rmtree(master_mock): master_mock.rmtree.side_effect = (OSError(-1, 'Fake'),) d = None try: - with directories.temp_dir() as d: + with temporary.temp_dir() as d: pass finally: - eq_(isdir(d), True) - rmdir(d) + nose.tools.eq_(os.path.isdir(d), True) + os.rmdir(d) -@raises(DummyException) -@patch(directories, external=('tempfile.mkdtemp',)) +@nose.tools.raises(DummyException) +@simian.patch(temporary.directories, external=('tempfile.mkdtemp',)) def test_temp_dir_passes_through_mkdtemp_args(master_mock): master_mock.mkdtemp.side_effect = (DummyException(),) try: - with directories.temp_dir('suffix', 'prefix', 'parent_dir'): + with temporary.temp_dir('suffix', 'prefix', 'parent_dir'): pass # pragma: no cover except DummyException: master_mock.mkdtemp.assert_called_once_with( @@ -103,16 +109,6 @@ def test_temp_dir_passes_through_mkdtemp_args(master_mock): raise -def test_temp_dir_can_import_from_init(): - import temporary - assert temporary.temp_dir - - -def test_in_temp_dir_can_import_from_init(): - import temporary - assert temporary.in_temp_dir - - # # Test Helpers # @@ -121,18 +117,18 @@ def create_file_in_tree(path): try: touch(path) except IOError as e: - if e.errno != ENOENT: - raise # pragma: no cover - create_dir_tree(dirname(path)) + if e.errno != errno.ENOENT: + raise # pragma: no cover + create_dir_tree(os.path.dirname(path)) touch(path) def create_dir_tree(path): try: - makedirs(path) - except OSError as e: # pragma: no cover - if e.errno != EEXIST or not isdir(path): # pragma: no cover - raise # pragma: no cover + os.makedirs(path) + except OSError as e: # pragma: no cover + if e.errno != errno.EEXIST or not os.path.isdir(path): # pragma: no cover + raise # pragma: no cover def touch(path): diff --git a/temporary/test/test_files.py b/temporary/test/test_files.py index 7cd8316..341698e 100644 --- a/temporary/test/test_files.py +++ b/temporary/test/test_files.py @@ -1,11 +1,10 @@ -from nose.tools import eq_, raises -from os import remove -from os.path import dirname, exists, isfile -from simian import patch -from temporary import temp_dir +import os +import os.path -# module under test -from temporary import files +import nose.tools +import simian + +import temporary # @@ -21,47 +20,47 @@ class DummyException(Exception): # def test_temp_file_creates_and_deletes_file(): - with files.temp_file() as f_name: - eq_(isfile(f_name), True) - eq_(exists(f_name), False) + with temporary.temp_file() as f_name: + nose.tools.eq_(os.path.isfile(f_name), True) + nose.tools.eq_(os.path.exists(f_name), False) def test_temp_file_in_custom_parent_dir(): - with temp_dir() as parent_dir: - with files.temp_file(parent_dir=parent_dir) as f_name: - eq_(dirname(f_name), parent_dir) + with temporary.temp_dir() as parent_dir: + with temporary.temp_file(parent_dir=parent_dir) as f_name: + nose.tools.eq_(os.path.dirname(f_name), parent_dir) def test_temp_file_with_content(): - with files.temp_file('hello!') as f_name: + with temporary.temp_file('hello!') as f_name: with open(f_name) as f: assert f.read() == 'hello!' def test_temp_file_manually_deleted_is_allowed(): - with files.temp_file() as f_name: - remove(f_name) + with temporary.temp_file() as f_name: + os.remove(f_name) -@raises(OSError) +@nose.tools.raises(OSError) def test_temp_file_with_failed_remove(): - @patch(files, external=('os.remove',)) + @simian.patch(temporary.files, external=('os.remove',)) def blow_up(master_mock): master_mock.remove.side_effect = (OSError(-1, 'Fake'),) - with files.temp_file(parent_dir=parent_dir): + with temporary.temp_file(parent_dir=parent_dir): pass - with temp_dir() as parent_dir: + with temporary.temp_dir() as parent_dir: blow_up() # pylint: disable=no-value-for-parameter -@raises(DummyException) -@patch(files, external=('tempfile.mkstemp',)) +@nose.tools.raises(DummyException) +@simian.patch(temporary.files, external=('tempfile.mkstemp',)) def test_temp_file_passes_through_mkstemp_args(master_mock): master_mock.mkstemp.side_effect = (DummyException(),) try: - ctx = files.temp_file( + ctx = temporary.temp_file( suffix='suffix', prefix='prefix', parent_dir='parent_dir', @@ -72,8 +71,3 @@ def test_temp_file_passes_through_mkstemp_args(master_mock): master_mock.mkstemp.assert_called_once_with( 'suffix', 'prefix', 'parent_dir', True) raise - - -def test_temp_file_can_import_from_init(): - import temporary - assert temporary.temp_file diff --git a/temporary/test/test_tests_using_decorators.py b/temporary/test/test_tests_using_decorators.py new file mode 100644 index 0000000..a4d2308 --- /dev/null +++ b/temporary/test/test_tests_using_decorators.py @@ -0,0 +1,19 @@ +import os.path +import subprocess + + +def test_tests_using_decorators(): + # Assemble + test_path = os.path.join(os.path.dirname(__file__), 'tests_using_decorators', 'tests.py') + process = subprocess.Popen( + args=('nosetests', test_path), + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + # Act + out, _ = process.communicate() + status = process.returncode + + # Assert + assert b'Ran 3 tests' in out, "b'Ran 3 tests' not found in {!r}".format(out) + assert status == 0, 'status == {!r}, not 0'.format(status) diff --git a/temporary/test/tests_using_decorators/tests.py b/temporary/test/tests_using_decorators/tests.py new file mode 100644 index 0000000..7272cf4 --- /dev/null +++ b/temporary/test/tests_using_decorators/tests.py @@ -0,0 +1,15 @@ +import temporary + + +def test_control(): + pass + + +@temporary.temp_dir(make_cwd=True) +def test_temp_dir_with_make_cwd_is_registered_as_test(): + pass + + +@temporary.in_temp_dir() +def test_in_temp_dir_is_registered_as_test(): + pass diff --git a/temporary/util.py b/temporary/util.py new file mode 100644 index 0000000..b8df2e9 --- /dev/null +++ b/temporary/util.py @@ -0,0 +1,12 @@ +import errno + +import contextlib2 as contextlib + + +@contextlib.contextmanager +def allow_missing_file(): + try: + yield + except OSError as e: + if e.errno != errno.ENOENT: + raise diff --git a/tests.sh b/tests.sh new file mode 100755 index 0000000..47dabaa --- /dev/null +++ b/tests.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +set -e -o pipefail + +readonly PACKAGE_DIR="temporary" + +function each_iname { + local iname=${1}; shift + + find * -type f -iname "${iname}" | while read -r filename; do + "$@" "${filename}" + done +} + +function static_analysis { + each_iname "*.rst" rst2html.py --exit-status=2 > /dev/null + python setup.py check --strict --restructuredtext --metadata + flake8 setup.py "${PACKAGE_DIR}" + pylint --reports=no --rcfile=.pylintrc "${PACKAGE_DIR}" +} + +function unit_test { + nosetests \ + --exe \ + --with-doctest \ + --doctest-options="+NORMALIZE_WHITESPACE" \ + --with-coverage \ + --cover-tests \ + --cover-inclusive \ + --cover-package="${PACKAGE_DIR}" \ + "${PACKAGE_DIR}" +} + +function main { + if [ "${1}" == "--static-analysis" ]; then + static_analysis + fi + unit_test +} + +if [ "${BASH_SOURCE[0]}" == "${0}" ]; then + main "$@" +fi diff --git a/tox.ini b/tox.ini index f95dae8..1303f7a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,35 +1,38 @@ [tox] -envlist = py26,py27,py32,py33,py34,pypy,pypy3 -skipsdist = {env:TOXSKIPSDIST:false} +envlist = py26,py27,py33,py34,py35,pypy,pypy3 +skipsdist = {env:TOXBUILD:false} -[installonly] -flake8 = true -pyflakes = true -pylint = true -nosetests = true - -[defaultcommands] -flake8 = flake8 setup.py "temporary" -pyflakes = pyflakes setup.py "temporary" -pylint = pylint --rcfile=.pylintrc "temporary" -nosetests = nosetests --with-doctest --with-coverage --cover-tests --cover-inclusive --cover-package="temporary" "temporary" +[testenv:py27] +passenv = LANG +whitelist_externals = + true + bash +deps = + -rrequirements_test_runner.txt + -rrequirements_static_analysis.txt + -rrequirements_test.txt +commands = {env:TOXBUILD:bash ./tests.sh --static-analysis} [testenv:py34] -whitelist_externals = true +passenv = LANG +whitelist_externals = + true + bash deps = -rrequirements_test_runner.txt -rrequirements_static_analysis.txt -rrequirements_test.txt -commands = - {[{env:TOXCOMMANDS:defaultcommands}]flake8} - {[{env:TOXCOMMANDS:defaultcommands}]pyflakes} - {[{env:TOXCOMMANDS:defaultcommands}]pylint} - {[{env:TOXCOMMANDS:defaultcommands}]nosetests} +commands = {env:TOXBUILD:bash ./tests.sh --static-analysis} [testenv] -whitelist_externals = true +passenv = LANG +whitelist_externals = + true + bash deps = -rrequirements_test_runner.txt -rrequirements_test.txt -commands = - {[{env:TOXCOMMANDS:defaultcommands}]nosetests} +commands = {env:TOXBUILD:bash ./tests.sh} + +[flake8] +max-line-length = 120