diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..f9c71a7f --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +max-line-length = 120 +per-file-ignores = + **/*.pyi:E252,E301,E302,E305,E501,E701,E704,F401,F811,F821 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..0192a42a --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,68 @@ +name: build + +on: [push, pull_request] + +jobs: + build: + + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + python: ["2.7", "3.5", "3.6", "3.7", "3.8", "pypy3"] + os: [ubuntu-latest, windows-latest] + include: + - python: "2.7" + tox_env: "py27-pytest30" + - python: "3.5" + tox_env: "py35-pytest30" + - python: "3.6" + tox_env: "py36-pytest30" + - python: "3.7" + tox_env: "py37-pytest30" + - python: "3.8" + tox_env: "py38-pytest30" + - python: "pypy3" + tox_env: "pypy3-pytest30" + + steps: + - uses: actions/checkout@v1 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python }} + - name: Install tox + run: | + python -m pip install --upgrade pip + pip install tox + - name: Test + run: | + tox -e ${{ matrix.tox_env }} + + deploy: + + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + + runs-on: ubuntu-latest + + needs: build + + steps: + - uses: actions/checkout@v1 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: "3.7" + - name: Install wheel + run: | + python -m pip install --upgrade pip + pip install wheel + - name: Build package + run: | + python setup.py sdist bdist_wheel + - name: Publish package to PyPI + uses: pypa/gh-action-pypi-publish@master + with: + user: __token__ + password: ${{ secrets.pypi_token }} diff --git a/.gitignore b/.gitignore index 5bb0d457..fa936f15 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .cache/ .tox/ __pycache__/ +.mypy_cache/ *.pyc *.pyo @@ -10,3 +11,5 @@ __pycache__/ .eggs/ dist/* +/py/_version.py +.pytest_cache/ diff --git a/.hgignore b/.hgignore deleted file mode 100644 index 34976da5..00000000 --- a/.hgignore +++ /dev/null @@ -1,29 +0,0 @@ - -# Automatically generated by `hgimportsvn` -syntax:glob -.svn -.hgsvn - -# These lines are suggested according to the svn:ignore property -# Feel free to enable them by uncommenting them -syntax:glob -*.pyc -*.pyo -*.swp -*.html -*.class -*.orig -*~ - -doc/_build -build/ -dist/ -*.egg-info -issue/ -env/ -3rdparty/ -.tox -lib/ -bin/ -include/ -src/ diff --git a/.hgtags b/.hgtags deleted file mode 100644 index 9d48095b..00000000 --- a/.hgtags +++ /dev/null @@ -1,68 +0,0 @@ -52c6d9e78777a5a34e813123997dfc614a1a4767 1.0.0b3 -1c7aaa8c61f3b0945921a9acc7beb184201aed4b 1.0.0b4 -1c7aaa8c61f3b0945921a9acc7beb184201aed4b 1.0.0b4 -0000000000000000000000000000000000000000 1.0.0b4 -0000000000000000000000000000000000000000 1.0.0b4 -8cd6eb91eba313b012d6e568f37d844dc0751f2e 1.0.0b4 -8cd6eb91eba313b012d6e568f37d844dc0751f2e 1.0.0b4 -0000000000000000000000000000000000000000 1.0.0b4 -2cc0507f117ffe721dff7ee026648cfce00ec92f 1.0.0b6 -86f1e1b6e49bf5882a809f11edd1dbb08162cdad 1.0.0b8 -86f1e1b6e49bf5882a809f11edd1dbb08162cdad 1.0.0b8 -c63f35c266cbb26dad6b87b5e115d65685adf448 1.0.0b8 -c63f35c266cbb26dad6b87b5e115d65685adf448 1.0.0b8 -0eaa0fdf2ba0163cf534dc2eff4ba2e5fc66c261 1.0.0b8 -e2a60653cb490aeed81bbbd83c070b99401c211c 1.0.0b9 -5ea0cdf7854c3d4278d36eda94a2b68483a0e211 1.0.0 -5ea0cdf7854c3d4278d36eda94a2b68483a0e211 1.0.0 -7acde360d94b6a2690ce3d03ff39301da84c0a2b 1.0.0 -6bd221981ac99103002c1cb94fede400d23a96a1 1.0.1 -4816e8b80602a3fd3a0a120333ad85fbe7d8bab4 1.0.2 -60c44bdbf093285dc69d5462d4dbb4acad325ca6 1.1.0 -319187fcda66714c5eb1353492babeec3d3c826f 1.1.1 -4fc5212f7626a56b9eb6437b5c673f56dd7eb942 1.2.0 -c143a8c8840a1c68570890c8ac6165bbf92fd3c6 1.2.1 -eafd3c256e8732dfb0a4d49d051b5b4339858926 1.3.0 -d5eacf390af74553227122b85e20345d47b2f9e6 1.3.1 -d5eacf390af74553227122b85e20345d47b2f9e6 1.3.1 -8b8e7c25a13cf863f01b2dd955978285ae9daf6a 1.3.1 -3bff44b188a7ec1af328d977b9d39b6757bb38df 1.3.2 -c59d3fa8681a5b5966b8375b16fccd64a3a8dbeb 1.3.3 -79ef6377705184c55633d456832eea318fedcf61 1.3.4 -79ef6377705184c55633d456832eea318fedcf61 1.3.4 -90fffd35373e9f125af233f78b19416f0938d841 1.3.4 -5346ab41b059c95a48cbe1e8a7bae96ce6e0da27 1.4.0 -1f3125cba7976538952be268f107c1d0c36c5ce8 1.4.1 -04ab22db4ff737cf31e91d75a0f5d7077f324167 1.4.2 -9950bf9d684a984d511795013421c89c5cf88bef 1.4.3 -d9951e3bdbc765e73835ae13012f6a074d13d8bf 1.4.4 -b827dd156a36753e32c7f3f15ce82d6fe9e356c8 1.4.6 -f15726f9e5a67cc6221c499affa4840e9d591763 1.4.7 -abfabd07a1d328f13c730e8a50d80d2e470afd3b 1.4.9 -7f37ee0aff9be4b839d6759cfee336f60e8393a4 1.4.10 -fe4593263efa10ea7ba014db6e3379e0b82368a2 1.4.11 -f07af25a26786e4825b5170e17ad693245cb3426 1.4.12 -d3730d84ba7eda92fd3469a3f63fd6d8cb22c975 1.4.13 -12c1ae8e7c5345721e9ec9f8e27b1e36c07f74dc 1.4.14 -12c1ae8e7c5345721e9ec9f8e27b1e36c07f74dc 1.4.14 -0000000000000000000000000000000000000000 1.4.14 -0000000000000000000000000000000000000000 1.4.14 -1497e2efd0f8c73a0e3d529debf0c489e4cd6cab 1.4.14 -e065014c1ce8ad110a381e9baaaa5d647ba7ac6b 1.4.15 -e9e5b38f53dc35b35aa1f9ee9a9be9bbd2d2c3b1 1.4.16 -c603503945f52b78522d96a423605cbc953236d3 1.4.17 -c59201105a29801cc858eb9160b7a19791b91a35 1.4.18 -284cc172e294d48edc840012e1451c32c3963d92 1.4.19 -a3e0626aa0c5aecf271367dc77e476ab216ea3c8 1.4.20 -5e48016c4a3af8e7358a1267d33d021e71765bed 1.4.21 -01ae2cfcc61c4fcb3aa5031349adb5b467c31018 1.4.23 -5ffd982f4dff60b588f309cd9bdc61036547282a 1.4.24 -dc9ffbcaf1f7d72e96be3f68c11deebb7e7193c5 1.4.25 -6de1a44bf75de7af4fcae947c235e9072bbdbb9a 1.4.26 -7d650ba2657890a2253c8c4a83f170febebd90fa 1.4.27 -7d650ba2657890a2253c8c4a83f170febebd90fa 1.4.27 -1810003dec63dd1b506a23849861fffa5bc3ba13 1.4.27 -ba08706f08ddea1b77a426f00dfe2bdc244345e8 1.4.28 -4e8054ada63f3327bcf759ae7cd36c7c8652bc9b 1.4.29 -366ab346610c6de8aaa7617e24011794b40236c6 1.4.30 -657380e439f9b7e04918cb162cb2e46388244b42 1.4.31 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 30eaf50c..00000000 --- a/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -language: python -python: -- '2.6' -- '2.7' -- '3.3' -- '3.4' -- '3.5' -- pypy - -matrix: - allow_failures: - - python: pypy -cache: python -install: pip install tox-travis -script: tox diff --git a/AUTHORS b/AUTHORS index 8c0cf9b7..9c5dda9c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -22,3 +22,4 @@ Jan Balster Grig Gheorghiu Bob Ippolito Christian Tismer +Wim Glenn diff --git a/CHANGELOG b/CHANGELOG.rst similarity index 91% rename from CHANGELOG rename to CHANGELOG.rst index 2ea1f59f..90e5905a 100644 --- a/CHANGELOG +++ b/CHANGELOG.rst @@ -1,3 +1,118 @@ +1.10.0 (2020-12-12) +=================== + +- Fix a regular expression DoS vulnerability in the py.path.svnwc SVN blame functionality (CVE-2020-29651) +- Update vendored apipkg: 1.4 => 1.5 +- Update vendored iniconfig: 1.0.0 => 1.1.1 + +1.9.0 (2020-06-24) +================== + +- Add type annotation stubs for the following modules: + + * ``py.error`` + * ``py.iniconfig`` + * ``py.path`` (not including SVN paths) + * ``py.io`` + * ``py.xml`` + + There are no plans to type other modules at this time. + + The type annotations are provided in external .pyi files, not inline in the + code, and may therefore contain small errors or omissions. If you use ``py`` + in conjunction with a type checker, and encounter any type errors you believe + should be accepted, please report it in an issue. + +1.8.2 (2020-06-15) +================== + +- On Windows, ``py.path.local``s which differ only in case now have the same + Python hash value. Previously, such paths were considered equal but had + different hashes, which is not allowed and breaks the assumptions made by + dicts, sets and other users of hashes. + +1.8.1 (2019-12-27) +================== + +- Handle ``FileNotFoundError`` when trying to import pathlib in ``path.common`` + on Python 3.4 (#207). + +- ``py.path.local.samefile`` now works correctly in Python 3 on Windows when dealing with symlinks. + +1.8.0 (2019-02-21) +================== + +- add ``"importlib"`` pyimport mode for python3.5+, allowing unimportable test suites + to contain identically named modules. + +- fix ``LocalPath.as_cwd()`` not calling ``os.chdir()`` with ``None``, when + being invoked from a non-existing directory. + + +1.7.0 (2018-10-11) +================== + +- fix #174: use ``shutil.get_terminal_size()`` in Python 3.3+ to determine the size of the + terminal, which produces more accurate results than the previous method. + +- fix pytest-dev/pytest#2042: introduce new ``PY_IGNORE_IMPORTMISMATCH`` environment variable + that suppresses ``ImportMismatchError`` exceptions when set to ``1``. + + +1.6.0 (2018-08-27) +================== + +- add ``TerminalWriter.width_of_current_line`` (i18n version of + ``TerminalWriter.chars_on_current_line``), a read-only property + that tracks how wide the current line is, attempting to take + into account international characters in the calculation. + +1.5.4 (2018-06-27) +================== + +- fix pytest-dev/pytest#3451: don't make assumptions about fs case sensitivity + in ``make_numbered_dir``. + +1.5.3 +===== + +- fix #179: ensure we can support 'from py.error import ...' + +1.5.2 +===== + +- fix #169, #170: error importing py.log on Windows: no module named ``syslog``. + +1.5.1 +===== + +- fix #167 - prevent pip from installing py in unsupported Python versions. + +1.5.0 +===== + +NOTE: **this release has been removed from PyPI** due to missing package +metadata which caused a number of problems to py26 and py33 users. +This issue was fixed in the 1.5.1 release. + +- python 2.6 and 3.3 are no longer supported +- deprecate py.std and remove all internal uses +- fix #73 turn py.error into an actual module +- path join to / no longer produces leading double slashes +- fix #82 - remove unsupportable aliases +- fix python37 compatibility of path.sysfind on windows by correctly replacing vars +- turn iniconfig and apipkg into vendored packages and ease de-vendoring for distributions +- fix #68 remove invalid py.test.ensuretemp references +- fix #25 - deprecate path.listdir(sort=callable) +- add ``TerminalWriter.chars_on_current_line`` read-only property that tracks how many characters + have been written to the current line. + +1.4.34 +==================================================================== + +- fix issue119 / pytest issue708 where tmpdir may fail to make numbered directories + when the filesystem is case-insensitive. + 1.4.33 ==================================================================== @@ -34,7 +149,7 @@ 1.4.30 ================================================== -- fix issue68 an assert with a multiline list comprehension +- fix issue68 an assert with a multiline list comprehension was not reported correctly. Thanks Henrik Heibuerger. @@ -62,7 +177,7 @@ - allow a new ensuresyspath="append" mode for py.path.local.pyimport() so that a neccessary import path is appended instead of prepended to - sys.path + sys.path - strike undocumented, untested argument to py.path.local.pypkgpath @@ -110,10 +225,10 @@ thus triggering the alias module to resolve and blowing up with ImportError. The negative side is that something like "py.test.X" will now result in None instead of "importerror: pytest" - if pytest is not installed. But you shouldn't import "py.test" + if pytest is not installed. But you shouldn't import "py.test" anyway anymore. -- adapt one svn test to only check for any exception instead +- adapt one svn test to only check for any exception instead of specific ones because different svn versions cause different errors and we don't care. @@ -136,8 +251,8 @@ its output even if it didn't flush itself. - refactor traceback generation in light of pytest issue 364 - (shortening tracebacks). you can now set a new traceback style - on a per-entry basis such that a caller can force entries to be + (shortening tracebacks). you can now set a new traceback style + on a per-entry basis such that a caller can force entries to be isplayed as short or long entries. - win32: py.path.local.sysfind(name) will preferrably return files with @@ -149,7 +264,7 @@ - ignore unicode decode errors in xmlescape. Thanks Anatoly Bubenkoff. -- on python2 modify traceback.format_exception_only to match python3 +- on python2 modify traceback.format_exception_only to match python3 behaviour, namely trying to print unicode for Exception instances - use a safer way for serializing exception reports (helps to fix @@ -179,7 +294,7 @@ Changes between 1.4.17 and 1.4.18 - introduce path.ensure_dir() as a synonym for ensure(..., dir=1) - some unicode/python3 related fixes wrt to path manipulations - (if you start passing unicode particular in py2 you might + (if you start passing unicode particular in py2 you might still get problems, though) Changes between 1.4.16 and 1.4.17 @@ -246,7 +361,7 @@ Changes between 1.4.12 and 1.4.13 Changes between 1.4.11 and 1.4.12 ================================================== -- fix python2.4 support - for pre-AST interpreters re-introduce +- fix python2.4 support - for pre-AST interpreters re-introduce old way to find statements in exceptions (closes pytest issue 209) - add tox.ini to distribution - fix issue23 - print *,** args information in tracebacks, @@ -264,7 +379,7 @@ Changes between 1.4.10 and 1.4.11 unicodeencode/decode problems, amend according test - introduce py.builtin.text and py.builtin.bytes to point to respective str/unicode (py2) and bytes/str (py3) types -- fix error handling on win32/py33 for ENODIR +- fix error handling on win32/py33 for ENODIR Changes between 1.4.9 and 1.4.10 ================================================== @@ -301,12 +416,12 @@ Changes between 1.4.6 and 1.4.7 Changes between 1.4.5 and 1.4.6 ================================================== -- help to fix pytest issue99: unify output of +- help to fix pytest issue99: unify output of ExceptionInfo.getrepr(style="native") with ...(style="long") - fix issue7: source.getstatementrange() now raises proper error if no valid statement can be found -- fix issue8: fix code and tests of svnurl/svnwc to work on subversion 1.7 - - note that path.status(updates=1) will not properly work svn-17's status +- fix issue8: fix code and tests of svnurl/svnwc to work on subversion 1.7 - + note that path.status(updates=1) will not properly work svn-17's status --xml output is broken. - make source.getstatementrange() more resilent about non-python code frames (as seen from jnja2) @@ -375,7 +490,7 @@ Changes between 1.3.4 and 1.4.0 - py.test was moved to a separate "pytest" package. What remains is a stub hook which will proxy ``import py.test`` to ``pytest``. -- all command line tools ("py.cleanup/lookup/countloc/..." moved +- all command line tools ("py.cleanup/lookup/countloc/..." moved to "pycmd" package) - removed the old and deprecated "py.magic" namespace - use apipkg-1.1 and make py.apipkg.initpkg|ApiModule available diff --git a/MANIFEST.in b/MANIFEST.in index 239ad228..6d255b1a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,11 @@ -include CHANGELOG +include CHANGELOG.rst include AUTHORS include README.rst include setup.py include LICENSE include conftest.py include tox.ini +recursive-include py *.pyi graft doc graft testing global-exclude *.pyc diff --git a/README.rst b/README.rst index e44ab742..80800b2b 100644 --- a/README.rst +++ b/README.rst @@ -1,20 +1,30 @@ -.. image:: https://img.shields.io/pypi/pyversions/pytest.svg +.. image:: https://img.shields.io/pypi/v/py.svg + :target: https://pypi.org/project/py + +.. image:: https://img.shields.io/conda/vn/conda-forge/py.svg + :target: https://anaconda.org/conda-forge/py + +.. image:: https://img.shields.io/pypi/pyversions/py.svg :target: https://pypi.org/project/py -.. image:: https://img.shields.io/travis/pytest-dev/py.svg - :target: https://travis-ci.org/pytest-dev/py + +.. image:: https://github.com/pytest-dev/py/workflows/build/badge.svg + :target: https://github.com/pytest-dev/py/actions + + +**NOTE**: this library is in **maintenance mode** and should not be used in new code. The py lib is a Python development support library featuring the following tools and modules: -* ``py.path``: uniform local and svn path objects -* ``py.apipkg``: explicit API control and lazy-importing -* ``py.iniconfig``: easy parsing of .ini files -* ``py.code``: dynamic code generation and introspection +* ``py.path``: uniform local and svn path objects -> please use pathlib/pathlib2 instead +* ``py.apipkg``: explicit API control and lazy-importing -> please use the standalone package instead +* ``py.iniconfig``: easy parsing of .ini files -> please use the standalone package instead +* ``py.code``: dynamic code generation and introspection (deprecated, moved to ``pytest`` as a implementation detail). -NOTE: prior to the 1.4 release this distribution used to -contain py.test which is now its own package, see http://pytest.org +**NOTE**: prior to the 1.4 release this distribution used to +contain py.test which is now its own package, see https://docs.pytest.org -For questions and more information please visit http://py.readthedocs.org +For questions and more information please visit https://py.readthedocs.io Bugs and issues: https://github.com/pytest-dev/py diff --git a/RELEASING.rst b/RELEASING.rst new file mode 100644 index 00000000..fb588e3a --- /dev/null +++ b/RELEASING.rst @@ -0,0 +1,17 @@ +Release Procedure +----------------- + +#. Create a branch ``release-X.Y.Z`` from the latest ``master``. + +#. Manually update the ``CHANGELOG.rst`` and commit. + +#. Open a PR for this branch targeting ``master``. + +#. After all tests pass and the PR has been approved by at least another maintainer, publish to PyPI by creating and pushing a tag:: + + git tag X.Y.Z + git push git@github.com:pytest-dev/py X.Y.Z + + Wait for the deploy to complete, then make sure it is `available on PyPI `_. + +#. Merge your PR to ``master``. diff --git a/bench/localpath.py b/bench/localpath.py index ad4fbd8e..aad44f2e 100644 --- a/bench/localpath.py +++ b/bench/localpath.py @@ -1,6 +1,4 @@ - import py -import timeit class Listdir: numiter = 100000 @@ -70,6 +68,6 @@ def run(self): for i in xrange(cls.numiter): inst.run() elapsed = time.time() - now - print "%s: %d loops took %.2f seconds, per call %.6f" %( + print("%s: %d loops took %.2f seconds, per call %.6f" %( cls.__name__, - cls.numiter, elapsed, elapsed / cls.numiter) + cls.numiter, elapsed, elapsed / cls.numiter)) diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..a0a30858 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,7 @@ +coverage: + status: + project: true + patch: true + changes: true + +comment: off diff --git a/conftest.py b/conftest.py index 11c2d442..5bff3fe0 100644 --- a/conftest.py +++ b/conftest.py @@ -1,46 +1,32 @@ import py +import pytest import sys -pytest_plugins = 'doctest pytester'.split() +pytest_plugins = 'doctest', 'pytester' collect_ignore = ['build', 'doc/_build'] -import os, py -pid = os.getpid() - def pytest_addoption(parser): group = parser.getgroup("pylib", "py lib testing options") group.addoption('--runslowtests', action="store_true", dest="runslowtests", default=False, help=("run slow tests")) -def pytest_funcarg__sshhost(request): +@pytest.fixture +def sshhost(request): val = request.config.getvalue("sshhost") if val: return val py.test.skip("need --sshhost option") -def pytest_generate_tests(metafunc): - multi = getattr(metafunc.function, 'multi', None) - if multi is not None: - assert len(multi.kwargs) == 1 - for name, l in multi.kwargs.items(): - for val in l: - metafunc.addcall(funcargs={name: val}) - elif 'anypython' in metafunc.funcargnames: - for name in ('python2.4', 'python2.5', 'python2.6', - 'python2.7', 'python3.1', 'pypy-c', 'jython'): - metafunc.addcall(id=name, param=name) + # XXX copied from execnet's conftest.py - needs to be merged winpymap = { 'python2.7': r'C:\Python27\python.exe', - 'python2.6': r'C:\Python26\python.exe', - 'python2.5': r'C:\Python25\python.exe', - 'python2.4': r'C:\Python24\python.exe', - 'python3.1': r'C:\Python31\python.exe', } + def getexecutable(name, cache={}): try: return cache[name] @@ -49,7 +35,8 @@ def getexecutable(name, cache={}): if executable: if name == "jython": import subprocess - popen = subprocess.Popen([str(executable), "--version"], + popen = subprocess.Popen( + [str(executable), "--version"], universal_newlines=True, stderr=subprocess.PIPE) out, err = popen.communicate() if not err or "2.5" not in err: @@ -57,7 +44,9 @@ def getexecutable(name, cache={}): cache[name] = executable return executable -def pytest_funcarg__anypython(request): + +@pytest.fixture(params=('python2.7', 'pypy-c', 'jython')) +def anypython(request): name = request.param executable = getexecutable(name) if executable is None: diff --git a/doc/announce/release-0.9.2.txt b/doc/announce/release-0.9.2.txt index bc2d2ef2..8340dc44 100644 --- a/doc/announce/release-0.9.2.txt +++ b/doc/announce/release-0.9.2.txt @@ -16,7 +16,7 @@ Here is a quick summary of what the py lib provides: See here for more information: -Pypi pages: http://pypi.python.org/pypi/py/ +Pypi pages: https://pypi.org/project/py/ Download/Install: http://codespeak.net/py/0.9.2/download.html diff --git a/doc/announce/release-1.0.0.txt b/doc/announce/release-1.0.0.txt index 7024255a..aef25ec2 100644 --- a/doc/announce/release-1.0.0.txt +++ b/doc/announce/release-1.0.0.txt @@ -58,6 +58,6 @@ holger .. _`default plugins`: http://codespeak.net/py/dist/test/plugin/index.html .. _`distributed testing`: http://codespeak.net/py/dist/test/dist.html .. _`elastic distributed execution`: http://codespeak.net/py/dist/execnet.html -.. _`1.0.0 py lib release`: http://pypi.python.org/pypi/py +.. _`1.0.0 py lib release`: https://pypi.org/project/py/ .. _`oejskit`: http://codespeak.net/py/dist/test/plugin/oejskit.html diff --git a/doc/announce/release-1.4.0.txt b/doc/announce/release-1.4.0.txt index 6f9a7714..1c9fa756 100644 --- a/doc/announce/release-1.4.0.txt +++ b/doc/announce/release-1.4.0.txt @@ -22,7 +22,7 @@ as "pytest-2.0.0", see here for the revamped docs: http://pytest.org And "py.cleanup|py.lookup|py.countloc" etc. helpers are now part of -the pycmd distribution, see http://pypi.python.org/pypi/pycmd +the pycmd distribution, see https://pypi.org/project/pycmd/ This makes "py-1.4.0" a simple library which does not install any command line utilities anymore. diff --git a/doc/announce/release-1.4.1.txt b/doc/announce/release-1.4.1.txt index a5aa76b1..6ed72aa4 100644 --- a/doc/announce/release-1.4.1.txt +++ b/doc/announce/release-1.4.1.txt @@ -23,7 +23,7 @@ comes as its own separate "pytest" distribution, see: http://pytest.org Also, the "py.cleanup|py.lookup|py.countloc" helpers are now part of -the pycmd distribution, see http://pypi.python.org/pypi/pycmd +the pycmd distribution, see https://pypi.org/project/pycmd/ Changes between 1.4.0 and 1.4.1 diff --git a/doc/changelog.txt b/doc/changelog.txt index 237daca3..0c9d0928 100644 --- a/doc/changelog.txt +++ b/doc/changelog.txt @@ -1,3 +1,3 @@ .. _`changelog`: -.. include:: ../CHANGELOG +.. include:: ../CHANGELOG.rst diff --git a/doc/example/genhtml.py b/doc/example/genhtml.py index b5c8f525..7a6d4934 100644 --- a/doc/example/genhtml.py +++ b/doc/example/genhtml.py @@ -8,6 +8,6 @@ html.body( [html.p(p) for p in paras])) -print unicode(doc).encode('latin1') +print(unicode(doc).encode('latin1')) diff --git a/doc/example/genhtmlcss.py b/doc/example/genhtmlcss.py index 3e6d0af5..facca77b 100644 --- a/doc/example/genhtmlcss.py +++ b/doc/example/genhtmlcss.py @@ -20,4 +20,4 @@ class p(html.p): ) ) -print doc.unicode(indent=2) +print(doc.unicode(indent=2)) diff --git a/doc/example/genxml.py b/doc/example/genxml.py index 5f754e88..444a4ca5 100644 --- a/doc/example/genxml.py +++ b/doc/example/genxml.py @@ -12,6 +12,6 @@ class ns(py.xml.Namespace): ns.title("Java for Python programmers"),), publisher="N.N", ) -print doc.unicode(indent=2).encode('utf8') +print(doc.unicode(indent=2).encode('utf8')) diff --git a/doc/faq.txt b/doc/faq.txt index 52cb4b3f..6d374e1d 100644 --- a/doc/faq.txt +++ b/doc/faq.txt @@ -41,9 +41,9 @@ as a clone of ``py.test`` when py.test was in the ``0.8`` release cycle so some of the newer features_ introduced with py.test-1.0 and py.test-1.1 have no counterpart in nose_. -.. _nose: http://somethingaboutorange.com/mrl/projects/nose/0.11.1/ +.. _nose: https://nose.readthedocs.io/ .. _features: test/features.html -.. _apipkg: http://pypi.python.org/pypi/apipkg +.. _apipkg: https://pypi.org/project/apipkg/ What's this "magic" with py.test? @@ -112,7 +112,7 @@ and will safely find all factory functions for the ``MYARG`` function argument. It helps to alleviate the de-coupling of function argument usage and creation. -.. _`Convention over Configuration`: http://en.wikipedia.org/wiki/Convention_over_Configuration +.. _`Convention over Configuration`: https://en.wikipedia.org/wiki/Convention_over_configuration Can I yield multiple values from a factory function? ----------------------------------------------------- @@ -134,7 +134,7 @@ Use the `pytest_generate_tests`_ hook to solve both issues and implement the `parametrization scheme of your choice`_. .. _`pytest_generate_tests`: test/funcargs.html#parametrizing-tests -.. _`parametrization scheme of your choice`: http://tetamap.wordpress.com/2009/05/13/parametrizing-python-tests-generalized/ +.. _`parametrization scheme of your choice`: https://holgerkrekel.net/2009/05/13/parametrizing-python-tests-generalized/ py.test interaction with other packages @@ -167,6 +167,4 @@ script with this content and invoke that with the python version:: .. _`directly use a checkout`: install.html#directly-use-a-checkout -.. _`install distribute`: http://pypi.python.org/pypi/distribute#installation-instructions - - +.. _`install distribute`: https://pypi.org/project/distribute/ diff --git a/doc/install.txt b/doc/install.txt index d0e981de..5b662e0d 100644 --- a/doc/install.txt +++ b/doc/install.txt @@ -1,13 +1,13 @@ .. _`py`: -.. _`index page`: http://pypi.python.org/pypi/py/ +.. _`index page`: https://pypi.org/project/py/ installation info in a nutshell =================================================== **PyPI name**: py_ -**Pythons**: CPython 2.6, 2.7, 3.3, 3.4, PyPy-2.3 +**Pythons**: CPython 2.7, 3.5, 3.6, 3.7, PyPy-5.4 **Operating systems**: Linux, Windows, OSX, Unix @@ -15,7 +15,7 @@ installation info in a nutshell **Installers**: ``easy_install`` and ``pip`` -**hg repository**: https://bitbucket.org/hpk42/py +**Code repository**: https://github.com/pytest-dev/py easy install or pip ``py`` ----------------------------- @@ -39,16 +39,16 @@ Working from version control or a tarball ----------------------------------------------- To follow development or start experiments, checkout the -complete code and documentation source with mercurial_:: +complete code and documentation source:: - hg clone https://bitbucket.org/hpk42/py + git clone https://github.com/pytest-dev/py -Development takes place on the 'trunk' branch. +Development takes place on the 'master' branch. You can also go to the python package index and download and unpack a TAR file:: - http://pypi.python.org/pypi/py/ + https://pypi.org/project/py/ activating a checkout with setuptools -------------------------------------------- @@ -63,7 +63,7 @@ in order to work inline with the tools and the lib of your checkout. .. _`directly use a checkout`: -.. _`setuptools`: http://pypi.python.org/pypi/setuptools +.. _`setuptools`: https://pypi.org/project/setuptools/ Mailing list and issue tracker @@ -73,10 +73,10 @@ Mailing list and issue tracker - #pylib on irc.freenode.net IRC channel for random questions. -- `bitbucket issue tracker`_ use this bitbucket issue tracker to report +- `issue tracker`_ use the issue tracker to report bugs or request features. -.. _`bitbucket issue tracker`: http://bitbucket.org/hpk42/py/issues/ +.. _`issue tracker`: https://github.com/pytest-dev/py/issues .. _codespeak: http://codespeak.net/ .. _`py-dev`: diff --git a/doc/links.inc b/doc/links.inc index 9bcfe5cf..b61d01c6 100644 --- a/doc/links.inc +++ b/doc/links.inc @@ -1,16 +1,15 @@ .. _`skipping plugin`: plugin/skipping.html .. _`funcargs mechanism`: funcargs.html -.. _`doctest.py`: http://docs.python.org/library/doctest.html +.. _`doctest.py`: https://docs.python.org/library/doctest.html .. _`xUnit style setup`: xunit_setup.html .. _`pytest_nose`: plugin/nose.html .. _`reStructured Text`: http://docutils.sourceforge.net .. _`Python debugger`: http://docs.python.org/lib/module-pdb.html -.. _nose: http://somethingaboutorange.com/mrl/projects/nose/ -.. _pytest: http://pypi.python.org/pypi/pytest -.. _mercurial: http://mercurial.selenic.com/wiki/ -.. _`setuptools`: http://pypi.python.org/pypi/setuptools -.. _`distribute`: http://pypi.python.org/pypi/distribute -.. _`pip`: http://pypi.python.org/pypi/pip -.. _`virtualenv`: http://pypi.python.org/pypi/virtualenv +.. _nose: https://nose.readthedocs.io/ +.. _pytest: https://pypi.org/project/pytest/ +.. _`setuptools`: https://pypi.org/project/setuptools/ +.. _`distribute`: https://pypi.org/project/distribute/ +.. _`pip`: https://pypi.org/project/pip/ +.. _`virtualenv`: https://pypi.org/project/virtualenv/ .. _hudson: http://hudson-ci.org/ diff --git a/doc/misc.txt b/doc/misc.txt index 8c3c0b3f..4b453482 100644 --- a/doc/misc.txt +++ b/doc/misc.txt @@ -90,4 +90,4 @@ right version:: Cross-Python Version compatibility helpers ============================================= -The ``py.builtin`` namespace provides a number of helpers that help to write python code compatible across Python interpreters, mainly Python2 and Python3. Type ``help(py.builtin)`` on a Python prompt for a the selection of builtins. +The ``py.builtin`` namespace provides a number of helpers that help to write python code compatible across Python interpreters, mainly Python2 and Python3. Type ``help(py.builtin)`` on a Python prompt for the selection of builtins. diff --git a/doc/path.txt b/doc/path.txt index 837c1d19..8f506d49 100644 --- a/doc/path.txt +++ b/doc/path.txt @@ -2,6 +2,12 @@ py.path ======= + **Note**: The 'py' library is in "maintenance mode" and so is not + recommended for new projects. Please check out + `pathlib `_ or + `pathlib2 `_ for path + operations. + The 'py' lib provides a uniform high-level api to deal with filesystems and filesystem-like interfaces: ``py.path``. It aims to offer a central object to fs-like object trees (reading from and writing to files, adding @@ -21,13 +27,11 @@ filesystem. It's just a bit nicer in usage than the regular Python APIs, and of course all the functionality is bundled together rather than spread over a number of modules. -Example usage, here we use the ``py.test.ensuretemp()`` function to create -a ``py.path.local`` object for us (which wraps a directory): .. sourcecode:: pycon >>> import py - >>> temppath = py.test.ensuretemp('py.path_documentation') + >>> temppath = py.path.local('py.path_documentation') >>> foopath = temppath.join('foo') # get child 'foo' (lazily) >>> foopath.check() # check if child 'foo' exists False @@ -77,7 +81,7 @@ Example usage of ``py.path.svnwc``: .. sourcecode:: pycon .. >>> if not py.test.config.option.urlcheck: raise ValueError('skipchunk') - >>> temp = py.test.ensuretemp('py.path_documentation') + >>> temp = py.path.local('py.path_documentation') >>> wc = py.path.svnwc(temp.join('svnwc')) >>> wc.checkout('http://codespeak.net/svn/py/dist/py/path/local') >>> wc.join('local.py').check() diff --git a/doc/style.css b/doc/style.css index 1faf762c..95e3ef07 100644 --- a/doc/style.css +++ b/doc/style.css @@ -546,7 +546,7 @@ div.message { } strong.highlight { background-color: #FFBBBB; -/* as usual, NetScape fucks up with innocent CSS +/* as usual, NetScape breaks with innocent CSS border-color: #FFAAAA; border-style: solid; border-width: 1pt; diff --git a/py/__init__.py b/py/__init__.py index 7dc3d5a4..b892ce1a 100644 --- a/py/__init__.py +++ b/py/__init__.py @@ -8,21 +8,27 @@ (c) Holger Krekel and others, 2004-2014 """ -__version__ = '1.4.33' +from py._error import error -from py import _apipkg +try: + from py._vendored_packages import apipkg + lib_not_mangled_by_packagers = True + vendor_prefix = '._vendored_packages.' +except ImportError: + import apipkg + lib_not_mangled_by_packagers = False + vendor_prefix = '' -# so that py.error.* instances are picklable -import sys -sys.modules['py.error'] = _apipkg.AliasModule("py.error", "py._error", 'error') -import py.error # "Dereference" it now just to be safe (issue110) +try: + from ._version import version as __version__ +except ImportError: + # broken installation, we don't even try + __version__ = "unknown" -_apipkg.initpkg(__name__, attr={'_apipkg': _apipkg}, exportdefs={ +apipkg.initpkg(__name__, attr={'_apipkg': apipkg, 'error': error}, exportdefs={ # access to all standard lib modules 'std': '._std:std', - # access to all posix errno's as classes - 'error': '._error:error', '_pydir' : '.__metainfo:pydir', 'version': 'py:__version__', # backward compatibility @@ -30,8 +36,6 @@ # pytest-2.0 has a flat namespace, we use alias modules # to keep old references compatible 'test' : 'pytest', - 'test.collect' : 'pytest', - 'test.cmdline' : 'pytest', # hook into the top-level standard library 'process' : { @@ -42,13 +46,13 @@ }, 'apipkg' : { - 'initpkg' : '._apipkg:initpkg', - 'ApiModule' : '._apipkg:ApiModule', + 'initpkg' : vendor_prefix + 'apipkg:initpkg', + 'ApiModule' : vendor_prefix + 'apipkg:ApiModule', }, 'iniconfig' : { - 'IniConfig' : '._iniconfig:IniConfig', - 'ParseError' : '._iniconfig:ParseError', + 'IniConfig' : vendor_prefix + 'iniconfig:IniConfig', + 'ParseError' : vendor_prefix + 'iniconfig:ParseError', }, 'path' : { diff --git a/py/__init__.pyi b/py/__init__.pyi new file mode 100644 index 00000000..96859e31 --- /dev/null +++ b/py/__init__.pyi @@ -0,0 +1,20 @@ +from typing import Any + +# py allows to use e.g. py.path.local even without importing py.path. +# So import implicitly. +from . import error +from . import iniconfig +from . import path +from . import io +from . import xml + +__version__: str + +# Untyped modules below here. +std: Any +test: Any +process: Any +apipkg: Any +code: Any +builtin: Any +log: Any diff --git a/py/_builtin.py b/py/_builtin.py index 52ee9d79..ddc89fc7 100644 --- a/py/_builtin.py +++ b/py/_builtin.py @@ -1,120 +1,21 @@ import sys -try: - reversed = reversed -except NameError: - def reversed(sequence): - """reversed(sequence) -> reverse iterator over values of the sequence - - Return a reverse iterator - """ - if hasattr(sequence, '__reversed__'): - return sequence.__reversed__() - if not hasattr(sequence, '__getitem__'): - raise TypeError("argument to reversed() must be a sequence") - return reversed_iterator(sequence) - - class reversed_iterator(object): - - def __init__(self, seq): - self.seq = seq - self.remaining = len(seq) - - def __iter__(self): - return self - - def next(self): - i = self.remaining - if i > 0: - i -= 1 - item = self.seq[i] - self.remaining = i - return item - raise StopIteration - - def __length_hint__(self): - return self.remaining - -try: - any = any -except NameError: - def any(iterable): - for x in iterable: - if x: - return True - return False - -try: - all = all -except NameError: - def all(iterable): - for x in iterable: - if not x: - return False - return True - -try: - sorted = sorted -except NameError: - builtin_cmp = cmp # need to use cmp as keyword arg - - def sorted(iterable, cmp=None, key=None, reverse=0): - use_cmp = None - if key is not None: - if cmp is None: - def use_cmp(x, y): - return builtin_cmp(x[0], y[0]) - else: - def use_cmp(x, y): - return cmp(x[0], y[0]) - l = [(key(element), element) for element in iterable] - else: - if cmp is not None: - use_cmp = cmp - l = list(iterable) - if use_cmp is not None: - l.sort(use_cmp) - else: - l.sort() - if reverse: - l.reverse() - if key is not None: - return [element for (_, element) in l] - return l - -try: - set, frozenset = set, frozenset -except NameError: - from sets import set, frozenset - -# pass through -enumerate = enumerate - -try: - BaseException = BaseException -except NameError: - BaseException = Exception - -try: - GeneratorExit = GeneratorExit -except NameError: - class GeneratorExit(Exception): - """ This exception is never raised, it is there to make it possible to - write code compatible with CPython 2.5 even in lower CPython - versions.""" - pass - GeneratorExit.__module__ = 'exceptions' +# Passthrough for builtins supported with py27. +BaseException = BaseException +GeneratorExit = GeneratorExit _sysex = (KeyboardInterrupt, SystemExit, MemoryError, GeneratorExit) +all = all +any = any +callable = callable +enumerate = enumerate +reversed = reversed +set, frozenset = set, frozenset +sorted = sorted -try: - callable = callable -except NameError: - def callable(obj): - return hasattr(obj, "__call__") if sys.version_info >= (3, 0): - exec ("print_ = print ; exec_=exec") + exec("print_ = print ; exec_=exec") import builtins # some backward compatibility helpers @@ -131,13 +32,13 @@ def _totext(obj, encoding=None, errors=None): def _isbytes(x): return isinstance(x, bytes) + def _istext(x): return isinstance(x, str) text = str bytes = bytes - def _getimself(function): return getattr(function, '__self__', None) diff --git a/py/_code/_assertionnew.py b/py/_code/_assertionnew.py index afb1b31f..d03f29d8 100644 --- a/py/_code/_assertionnew.py +++ b/py/_code/_assertionnew.py @@ -10,27 +10,10 @@ from py._code.assertion import _format_explanation, BuiltinAssertionError -if sys.platform.startswith("java") and sys.version_info < (2, 5, 2): - # See http://bugs.jython.org/issue1497 - _exprs = ("BoolOp", "BinOp", "UnaryOp", "Lambda", "IfExp", "Dict", - "ListComp", "GeneratorExp", "Yield", "Compare", "Call", - "Repr", "Num", "Str", "Attribute", "Subscript", "Name", - "List", "Tuple") - _stmts = ("FunctionDef", "ClassDef", "Return", "Delete", "Assign", - "AugAssign", "Print", "For", "While", "If", "With", "Raise", - "TryExcept", "TryFinally", "Assert", "Import", "ImportFrom", - "Exec", "Global", "Expr", "Pass", "Break", "Continue") - _expr_nodes = set(getattr(ast, name) for name in _exprs) - _stmt_nodes = set(getattr(ast, name) for name in _stmts) - def _is_ast_expr(node): - return node.__class__ in _expr_nodes - def _is_ast_stmt(node): - return node.__class__ in _stmt_nodes -else: - def _is_ast_expr(node): - return isinstance(node, ast.expr) - def _is_ast_stmt(node): - return isinstance(node, ast.stmt) +def _is_ast_expr(node): + return isinstance(node, ast.expr) +def _is_ast_stmt(node): + return isinstance(node, ast.stmt) class Failure(Exception): diff --git a/py/_code/_assertionold.py b/py/_code/_assertionold.py index 4e81fb3e..1bb70a87 100644 --- a/py/_code/_assertionold.py +++ b/py/_code/_assertionold.py @@ -2,6 +2,7 @@ import sys, inspect from compiler import parse, ast, pycodegen from py._code.assertion import BuiltinAssertionError, _format_explanation +import types passthroughex = py.builtin._sysex @@ -470,7 +471,7 @@ def check(s, frame=None): def interpret(source, frame, should_fail=False): module = Interpretable(parse(source, 'exec').node) #print "got module", module - if isinstance(frame, py.std.types.FrameType): + if isinstance(frame, types.FrameType): frame = py.code.Frame(frame) try: module.run(frame) diff --git a/py/_code/assertion.py b/py/_code/assertion.py index 4ce80c75..ff164379 100644 --- a/py/_code/assertion.py +++ b/py/_code/assertion.py @@ -87,8 +87,4 @@ def __init__(self, *args): reinterpret_old = "old reinterpretation not available for py3" else: from py._code._assertionold import interpret as reinterpret_old -if sys.version_info >= (2, 6) or (sys.platform.startswith("java")): - from py._code._assertionnew import interpret as reinterpret -else: - reinterpret = reinterpret_old - +from py._code._assertionnew import interpret as reinterpret diff --git a/py/_code/code.py b/py/_code/code.py index f14c562a..dad79628 100644 --- a/py/_code/code.py +++ b/py/_code/code.py @@ -1,6 +1,6 @@ import py import sys -from inspect import CO_VARARGS, CO_VARKEYWORDS +from inspect import CO_VARARGS, CO_VARKEYWORDS, isclass builtin_repr = repr @@ -11,6 +11,9 @@ else: from py._code._py2traceback import format_exception_only +import traceback + + class Code(object): """ wrapper around Python code objects """ def __init__(self, rawcode): @@ -21,7 +24,7 @@ def __init__(self, rawcode): self.firstlineno = rawcode.co_firstlineno - 1 self.name = rawcode.co_name except AttributeError: - raise TypeError("not a code object: %r" %(rawcode,)) + raise TypeError("not a code object: %r" % (rawcode,)) self.raw = rawcode def __eq__(self, other): @@ -106,7 +109,7 @@ def exec_(self, code, **vars): """ f_locals = self.f_locals.copy() f_locals.update(vars) - py.builtin.exec_(code, self.f_globals, f_locals ) + py.builtin.exec_(code, self.f_globals, f_locals) def repr(self, object): """ return a 'safe' (non-recursive, one-line) string repr for 'object' @@ -130,6 +133,7 @@ def getargs(self, var=False): pass # this can occur when using Psyco return retval + class TracebackEntry(object): """ a single entry in a traceback """ @@ -153,7 +157,7 @@ def relline(self): return self.lineno - self.frame.code.firstlineno def __repr__(self): - return "" %(self.frame.code.path, self.lineno+1) + return "" % (self.frame.code.path, self.lineno+1) @property def statement(self): @@ -237,17 +241,19 @@ def __str__(self): raise except: line = "???" - return " File %r:%d in %s\n %s\n" %(fn, self.lineno+1, name, line) + return " File %r:%d in %s\n %s\n" % (fn, self.lineno+1, name, line) def name(self): return self.frame.code.raw.co_name name = property(name, None, None, "co_name of underlaying code") + class Traceback(list): """ Traceback objects encapsulate and offer higher level access to Traceback entries. """ Entry = TracebackEntry + def __init__(self, tb): """ initialize from given python traceback object. """ if hasattr(tb, 'tb_next'): @@ -362,7 +368,8 @@ def __init__(self, tup=None, exprinfo=None): self.traceback = py.code.Traceback(self.tb) def __repr__(self): - return "" % (self.typename, len(self.traceback)) + return "" % ( + self.typename, len(self.traceback)) def exconly(self, tryshort=False): """ return the exception as a string @@ -391,7 +398,7 @@ def _getreprcrash(self): return ReprFileLocation(path, lineno+1, exconly) def getrepr(self, showlocals=False, style="long", - abspath=False, tbfilter=True, funcargs=False): + abspath=False, tbfilter=True, funcargs=False): """ return str()able representation of this exception info. showlocals: show locals per traceback entry style: long|short|no|native traceback style @@ -401,13 +408,14 @@ def getrepr(self, showlocals=False, style="long", """ if style == 'native': return ReprExceptionInfo(ReprTracebackNative( - py.std.traceback.format_exception( + traceback.format_exception( self.type, self.value, self.traceback[0]._rawentry, )), self._getreprcrash()) - fmt = FormattedExcinfo(showlocals=showlocals, style=style, + fmt = FormattedExcinfo( + showlocals=showlocals, style=style, abspath=abspath, tbfilter=tbfilter, funcargs=funcargs) return fmt.repr_excinfo(self) @@ -419,7 +427,7 @@ def __str__(self): def __unicode__(self): entry = self.traceback[-1] loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly()) - return unicode(loc) + return loc.__unicode__() class FormattedExcinfo(object): @@ -428,7 +436,8 @@ class FormattedExcinfo(object): flow_marker = ">" fail_marker = "E" - def __init__(self, showlocals=False, style="long", abspath=True, tbfilter=True, funcargs=False): + def __init__(self, showlocals=False, style="long", + abspath=True, tbfilter=True, funcargs=False): self.showlocals = showlocals self.style = style self.tbfilter = tbfilter @@ -521,7 +530,7 @@ def repr_locals(self, locals): #else: # self._line("%-10s =\\" % (name,)) # # XXX - # py.std.pprint.pprint(value, stream=self.excinfowriter) + # pprint.pprint(value, stream=self.excinfowriter) return ReprLocals(lines) def repr_traceback_entry(self, entry, excinfo=None): @@ -779,7 +788,7 @@ def getrawcode(obj, trycall=True): obj = getattr(obj, 'f_code', obj) obj = getattr(obj, '__code__', obj) if trycall and not hasattr(obj, 'co_firstlineno'): - if hasattr(obj, '__call__') and not py.std.inspect.isclass(obj): + if hasattr(obj, '__call__') and not isclass(obj): x = getrawcode(obj.__call__, trycall=False) if hasattr(x, 'co_firstlineno'): return x diff --git a/py/_code/source.py b/py/_code/source.py index c8b668b2..7fc7b23a 100644 --- a/py/_code/source.py +++ b/py/_code/source.py @@ -193,7 +193,8 @@ def compile(self, filename=None, mode='exec', if flag & _AST_FLAG: return co lines = [(x + "\n") for x in self.lines] - py.std.linecache.cache[filename] = (1, None, lines, filename) + import linecache + linecache.cache[filename] = (1, None, lines, filename) return co # @@ -224,8 +225,8 @@ def getfslineno(obj): code = py.code.Code(obj) except TypeError: try: - fn = (py.std.inspect.getsourcefile(obj) or - py.std.inspect.getfile(obj)) + fn = (inspect.getsourcefile(obj) or + inspect.getfile(obj)) except TypeError: return "", -1 @@ -248,7 +249,7 @@ def getfslineno(obj): def findsource(obj): try: - sourcelines, lineno = py.std.inspect.findsource(obj) + sourcelines, lineno = inspect.findsource(obj) except py.builtin._sysex: raise except: @@ -334,8 +335,6 @@ def get_statement_startend2(lineno, node): def getstatementrange_ast(lineno, source, assertion=False, astnode=None): if astnode is None: content = str(source) - if sys.version_info < (2,7): - content += "\n" try: astnode = compile(content, "source", "exec", 1024) # 1024 for AST except ValueError: diff --git a/py/_error.py b/py/_error.py index 8ca339be..a6375de9 100644 --- a/py/_error.py +++ b/py/_error.py @@ -2,6 +2,7 @@ create errno-specific classes for IO or os calls. """ +from types import ModuleType import sys, os, errno class Error(EnvironmentError): @@ -31,7 +32,7 @@ def __str__(self): 5: errno.EACCES, # anything better? } -class ErrorMaker(object): +class ErrorMaker(ModuleType): """ lazily provides Exception classes for each possible POSIX errno (as defined per the 'errno' module). All such instances subclass EnvironmentError. @@ -86,4 +87,5 @@ def checked_call(self, func, *args, **kwargs): __tracebackhide__ = True -error = ErrorMaker() +error = ErrorMaker('py.error') +sys.modules[error.__name__] = error \ No newline at end of file diff --git a/py/_io/capture.py b/py/_io/capture.py index bc157ed9..cacf2fa7 100644 --- a/py/_io/capture.py +++ b/py/_io/capture.py @@ -13,7 +13,7 @@ class TextIO(StringIO): def write(self, data): if not isinstance(data, unicode): data = unicode(data, getattr(self, '_encoding', 'UTF-8'), 'replace') - StringIO.write(self, data) + return StringIO.write(self, data) else: TextIO = StringIO @@ -24,7 +24,7 @@ class BytesIO(StringIO): def write(self, data): if isinstance(data, unicode): raise TypeError("not a byte value: %r" %(data,)) - StringIO.write(self, data) + return StringIO.write(self, data) patchsysdict = {0: 'stdin', 1: 'stdout', 2: 'stderr'} @@ -266,7 +266,7 @@ def readouterr(self): err = self._readsnapshot(self.err.tmpfile) else: err = "" - return [out, err] + return out, err def _readsnapshot(self, f): f.seek(0) diff --git a/py/_io/terminalwriter.py b/py/_io/terminalwriter.py index 390e8ca7..be559867 100644 --- a/py/_io/terminalwriter.py +++ b/py/_io/terminalwriter.py @@ -5,9 +5,10 @@ """ -import sys, os +import sys, os, unicodedata import py py3k = sys.version_info[0] >= 3 +py33 = sys.version_info >= (3, 3) from py.builtin import text, bytes win32_and_ctypes = False @@ -24,16 +25,21 @@ def _getdimensions(): - import termios,fcntl,struct - call = fcntl.ioctl(1,termios.TIOCGWINSZ,"\000"*8) - height,width = struct.unpack( "hhhh", call ) [:2] - return height, width + if py33: + import shutil + size = shutil.get_terminal_size() + return size.lines, size.columns + else: + import termios, fcntl, struct + call = fcntl.ioctl(1, termios.TIOCGWINSZ, "\000" * 8) + height, width = struct.unpack("hhhh", call)[:2] + return height, width def get_terminal_width(): - height = width = 0 + width = 0 try: - height, width = _getdimensions() + _, width = _getdimensions() except py.builtin._sysex: raise except: @@ -53,6 +59,21 @@ def get_terminal_width(): terminal_width = get_terminal_width() +char_width = { + 'A': 1, # "Ambiguous" + 'F': 2, # Fullwidth + 'H': 1, # Halfwidth + 'N': 1, # Neutral + 'Na': 1, # Narrow + 'W': 2, # Wide +} + + +def get_line_width(text): + text = unicodedata.normalize('NFC', text) + return sum(char_width.get(unicodedata.east_asian_width(c), 1) for c in text) + + # XXX unify with _escaped func below def ansi_print(text, esc, file=None, newline=True, flush=False): if file is None: @@ -129,7 +150,7 @@ def __init__(self, file=None, stringio=False, encoding=None): if stringio: self.stringio = file = py.io.TextIO() else: - file = py.std.sys.stdout + from sys import stdout as file elif py.builtin.callable(file) and not ( hasattr(file, "write") and hasattr(file, "flush")): file = WriteFile(file, encoding=encoding) @@ -139,6 +160,8 @@ def __init__(self, file=None, stringio=False, encoding=None): self._file = file self.hasmarkup = should_do_markup(file) self._lastlen = 0 + self._chars_on_current_line = 0 + self._width_of_current_line = 0 @property def fullwidth(self): @@ -150,6 +173,29 @@ def fullwidth(self): def fullwidth(self, value): self._terminal_width = value + @property + def chars_on_current_line(self): + """Return the number of characters written so far in the current line. + + Please note that this count does not produce correct results after a reline() call, + see #164. + + .. versionadded:: 1.5.0 + + :rtype: int + """ + return self._chars_on_current_line + + @property + def width_of_current_line(self): + """Return an estimate of the width so far in the current line. + + .. versionadded:: 1.6.0 + + :rtype: int + """ + return self._width_of_current_line + def _escaped(self, text, esc): if esc and self.hasmarkup: text = (''.join(['\x1b[%sm' % cod for cod in esc]) + @@ -181,7 +227,7 @@ def sep(self, sepchar, title=None, fullwidth=None, **kw): # i.e. 2 + 2*len(sepchar)*N + len(title) <= fullwidth # 2*len(sepchar)*N <= fullwidth - len(title) - 2 # N <= (fullwidth - len(title) - 2) // (2*len(sepchar)) - N = (fullwidth - len(title) - 2) // (2*len(sepchar)) + N = max((fullwidth - len(title) - 2) // (2*len(sepchar)), 1) fill = sepchar * N line = "%s %s %s" % (fill, title, fill) else: @@ -200,12 +246,27 @@ def write(self, msg, **kw): if msg: if not isinstance(msg, (bytes, text)): msg = text(msg) + + self._update_chars_on_current_line(msg) + if self.hasmarkup and kw: markupmsg = self.markup(msg, **kw) else: markupmsg = msg write_out(self._file, markupmsg) + def _update_chars_on_current_line(self, text_or_bytes): + newline = b'\n' if isinstance(text_or_bytes, bytes) else '\n' + current_line = text_or_bytes.rsplit(newline, 1)[-1] + if isinstance(current_line, bytes): + current_line = current_line.decode('utf-8', errors='replace') + if newline in text_or_bytes: + self._chars_on_current_line = len(current_line) + self._width_of_current_line = get_line_width(current_line) + else: + self._chars_on_current_line += len(current_line) + self._width_of_current_line += get_line_width(current_line) + def line(self, s='', **kw): self.write(s, **kw) self._checkfill(s) @@ -229,6 +290,9 @@ def write(self, msg, **kw): if msg: if not isinstance(msg, (bytes, text)): msg = text(msg) + + self._update_chars_on_current_line(msg) + oldcolors = None if self.hasmarkup and kw: handle = GetStdHandle(STD_OUTPUT_HANDLE) diff --git a/py/_log/log.py b/py/_log/log.py index ce47e8c7..56969bcb 100644 --- a/py/_log/log.py +++ b/py/_log/log.py @@ -14,7 +14,9 @@ debug=py.log.STDOUT, command=None) """ -import py, sys +import py +import sys + class Message(object): def __init__(self, keywords, args): @@ -70,6 +72,7 @@ def __init__(self): def getstate(self): return self.keywords2consumer.copy() + def setstate(self, state): self.keywords2consumer.clear() self.keywords2consumer.update(state) @@ -104,17 +107,22 @@ def setconsumer(self, keywords, consumer): consumer = File(consumer) self.keywords2consumer[keywords] = consumer + def default_consumer(msg): """ the default consumer, prints the message to stdout (using 'print') """ sys.stderr.write(str(msg)+"\n") default_keywordmapper = KeywordMapper() + def setconsumer(keywords, consumer): default_keywordmapper.setconsumer(keywords, consumer) + def setstate(state): default_keywordmapper.setstate(state) + + def getstate(): return default_keywordmapper.getstate() @@ -122,11 +130,12 @@ def getstate(): # Consumers # + class File(object): """ log consumer wrapping a file(-like) object """ def __init__(self, f): assert hasattr(f, 'write') - #assert isinstance(f, file) or not hasattr(f, 'open') + # assert isinstance(f, file) or not hasattr(f, 'open') self._file = f def __call__(self, msg): @@ -135,6 +144,7 @@ def __call__(self, msg): if hasattr(self._file, 'flush'): self._file.flush() + class Path(object): """ log consumer that opens and writes to a Path """ def __init__(self, filename, append=False, @@ -158,29 +168,39 @@ def __call__(self, msg): if not self._buffering: self._file.flush() + def STDOUT(msg): """ consumer that writes to sys.stdout """ sys.stdout.write(str(msg)+"\n") + def STDERR(msg): """ consumer that writes to sys.stderr """ sys.stderr.write(str(msg)+"\n") + class Syslog: """ consumer that writes to the syslog daemon """ - def __init__(self, priority = None): + def __init__(self, priority=None): if priority is None: priority = self.LOG_INFO self.priority = priority def __call__(self, msg): """ write a message to the log """ - py.std.syslog.syslog(self.priority, str(msg)) - -for _prio in "EMERG ALERT CRIT ERR WARNING NOTICE INFO DEBUG".split(): - _prio = "LOG_" + _prio - try: - setattr(Syslog, _prio, getattr(py.std.syslog, _prio)) - except AttributeError: - pass + import syslog + syslog.syslog(self.priority, str(msg)) + + +try: + import syslog +except ImportError: + pass +else: + for _prio in "EMERG ALERT CRIT ERR WARNING NOTICE INFO DEBUG".split(): + _prio = "LOG_" + _prio + try: + setattr(Syslog, _prio, getattr(syslog, _prio)) + except AttributeError: + pass diff --git a/py/_log/warning.py b/py/_log/warning.py index 722e31e9..6ef20d98 100644 --- a/py/_log/warning.py +++ b/py/_log/warning.py @@ -32,9 +32,11 @@ def _apiwarn(startversion, msg, stacklevel=2, function=None): msg = "%s (since version %s)" %(msg, startversion) warn(msg, stacklevel=stacklevel+1, function=function) + def warn(msg, stacklevel=1, function=None): if function is not None: - filename = py.std.inspect.getfile(function) + import inspect + filename = inspect.getfile(function) lineno = py.code.getrawcode(function).co_firstlineno else: try: @@ -67,10 +69,11 @@ def warn(msg, stacklevel=1, function=None): filename = module path = py.path.local(filename) warning = DeprecationWarning(msg, path, lineno) - py.std.warnings.warn_explicit(warning, category=Warning, + import warnings + warnings.warn_explicit(warning, category=Warning, filename=str(warning.path), lineno=warning.lineno, - registry=py.std.warnings.__dict__.setdefault( + registry=warnings.__dict__.setdefault( "__warningsregistry__", {}) ) diff --git a/py/_path/common.py b/py/_path/common.py index 0260e761..2364e5fe 100644 --- a/py/_path/common.py +++ b/py/_path/common.py @@ -1,12 +1,21 @@ """ """ -import os, sys, posixpath +import warnings +import os +import sys +import posixpath import fnmatch import py # Moved from local.py. iswin32 = sys.platform == "win32" or (getattr(os, '_name', False) == 'nt') +try: + # FileNotFoundError might happen in py34, and is not available with py27. + import_errors = (ImportError, FileNotFoundError) +except NameError: + import_errors = (ImportError,) + try: from os import fspath except ImportError: @@ -32,7 +41,7 @@ def fspath(path): raise try: import pathlib - except ImportError: + except import_errors: pass else: if isinstance(path, pathlib.PurePath): @@ -170,11 +179,16 @@ def read(self, mode='r'): def readlines(self, cr=1): """ read and return a list of lines from the path. if cr is False, the newline will be removed from the end of each line. """ + if sys.version_info < (3, ): + mode = 'rU' + else: # python 3 deprecates mode "U" in favor of "newline" option + mode = 'r' + if not cr: - content = self.read('rU') + content = self.read(mode) return content.split('\n') else: - f = self.open('rU') + f = self.open(mode) try: return f.readlines() finally: @@ -184,14 +198,16 @@ def load(self): """ (deprecated) return object unpickled from self.read() """ f = self.open('rb') try: - return py.error.checked_call(py.std.pickle.load, f) + import pickle + return py.error.checked_call(pickle.load, f) finally: f.close() def move(self, target): """ move this path to target. """ if target.relto(self): - raise py.error.EINVAL(target, + raise py.error.EINVAL( + target, "cannot move path into a subdirectory of itself") try: self.rename(target) @@ -221,7 +237,7 @@ def check(self, **kw): path.check(file=1, link=1) # a link pointing to a file """ if not kw: - kw = {'exists' : 1} + kw = {'exists': 1} return self.Checkers(self)._evaluate(kw) def fnmatch(self, pattern): @@ -370,6 +386,9 @@ def visit(self, fil=None, rec=None, ignore=NeverRaised, bf=False, sort=False): def _sortlist(self, res, sort): if sort: if hasattr(sort, '__call__'): + warnings.warn(DeprecationWarning( + "listdir(sort=callable) is deprecated and breaks on python3" + ), stacklevel=3) res.sort(sort) else: res.sort() diff --git a/py/_path/local.py b/py/_path/local.py index 0d4e4c93..1385a039 100644 --- a/py/_path/local.py +++ b/py/_path/local.py @@ -4,7 +4,7 @@ from __future__ import with_statement from contextlib import contextmanager -import sys, os, re, atexit, io +import sys, os, atexit, io, uuid import py from py._path import common from py._path.common import iswin32, fspath @@ -18,6 +18,11 @@ def map_as_list(func, iter): else: map_as_list = map +ALLOW_IMPORTLIB_MODE = sys.version_info > (3,5) +if ALLOW_IMPORTLIB_MODE: + import importlib + + class Stat(object): def __getattr__(self, name): return getattr(self._osstatresult, "st_" + name) @@ -158,7 +163,10 @@ def __init__(self, path=None, expanduser=False): self.strpath = abspath(path) def __hash__(self): - return hash(self.strpath) + s = self.strpath + if iswin32: + s = s.lower() + return hash(s) def __eq__(self, other): s1 = fspath(self) @@ -191,8 +199,8 @@ def samefile(self, other): other = abspath(other) if self == other: return True - if iswin32: - return False # there is no samefile + if not hasattr(os.path, "samefile"): + return False return py.error.checked_call( os.path.samefile, self.strpath, other) @@ -205,14 +213,16 @@ def remove(self, rec=1, ignore_errors=False): if rec: # force remove of readonly files on windows if iswin32: - self.chmod(448, rec=1) # octcal 0700 - py.error.checked_call(py.std.shutil.rmtree, self.strpath, + self.chmod(0o700, rec=1) + import shutil + py.error.checked_call( + shutil.rmtree, self.strpath, ignore_errors=ignore_errors) else: py.error.checked_call(os.rmdir, self.strpath) else: if iswin32: - self.chmod(448) # octcal 0700 + self.chmod(0o700) py.error.checked_call(os.remove, self.strpath) def computehash(self, hashtype="md5", chunksize=524288): @@ -333,13 +343,16 @@ def join(self, *args, **kwargs): strargs = newargs break newargs.insert(0, arg) + # special case for when we have e.g. strpath == "/" + actual_sep = "" if strpath.endswith(sep) else sep for arg in strargs: arg = arg.strip(sep) if iswin32: # allow unix style paths even on windows. arg = arg.strip('/') arg = arg.replace('/', sep) - strpath = strpath + sep + arg + strpath = strpath + actual_sep + arg + actual_sep = sep obj = object.__new__(self.__class__) obj.strpath = normpath(strpath) return obj @@ -448,8 +461,9 @@ def rename(self, target): def dump(self, obj, bin=1): """ pickle object into path location""" f = self.open('wb') + import pickle try: - py.error.checked_call(py.std.pickle.dump, obj, f, bin) + py.error.checked_call(pickle.dump, obj, f, bin) finally: f.close() @@ -568,14 +582,17 @@ def chdir(self): @contextmanager def as_cwd(self): - """ return context manager which changes to current dir during the - managed "with" context. On __enter__ it returns the old dir. + """ + Return a context manager, which changes to the path's dir during the + managed "with" context. + On __enter__ it returns the old dir, which might be ``None``. """ old = self.chdir() try: yield old finally: - old.chdir() + if old is not None: + old.chdir() def realpath(self): """ return a new path which contains no symbolic links.""" @@ -641,10 +658,35 @@ def pyimport(self, modname=None, ensuresyspath=True): If ensuresyspath=="append" the root dir will be appended if it isn't already contained in sys.path. if ensuresyspath is False no modification of syspath happens. + + Special value of ensuresyspath=="importlib" is intended + purely for using in pytest, it is capable only of importing + separate .py files outside packages, e.g. for test suite + without any __init__.py file. It effectively allows having + same-named test modules in different places and offers + mild opt-in via this option. Note that it works only in + recent versions of python. """ if not self.check(): raise py.error.ENOENT(self) + if ensuresyspath == 'importlib': + if modname is None: + modname = self.purebasename + if not ALLOW_IMPORTLIB_MODE: + raise ImportError( + "Can't use importlib due to old version of Python") + spec = importlib.util.spec_from_file_location( + modname, str(self)) + if spec is None: + raise ImportError( + "Can't find module %s at location %s" % + (modname, str(self)) + ) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + pkgpath = None if modname is None: pkgpath = self.pypkgpath() @@ -663,7 +705,7 @@ def pyimport(self, modname=None, ensuresyspath=True): mod = sys.modules[modname] if self.basename == "__init__.py": return mod # we don't check anything as we might - # we in a namespace package ... too icky to check + # be in a namespace package ... too icky to check modfile = mod.__file__ if modfile[-4:] in ('.pyc', '.pyo'): modfile = modfile[:-1] @@ -677,14 +719,17 @@ def pyimport(self, modname=None, ensuresyspath=True): except py.error.ENOENT: issame = False if not issame: - raise self.ImportMismatchError(modname, modfile, self) + ignore = os.getenv('PY_IGNORE_IMPORTMISMATCH') + if ignore != '1': + raise self.ImportMismatchError(modname, modfile, self) return mod else: try: return sys.modules[modname] except KeyError: # we have a custom modname, do a pseudo-import - mod = py.std.types.ModuleType(modname) + import types + mod = types.ModuleType(modname) mod.__file__ = str(self) sys.modules[modname] = mod try: @@ -729,7 +774,7 @@ def sysfind(cls, name, checker=None, paths=None): else: if paths is None: if iswin32: - paths = py.std.os.environ['Path'].split(';') + paths = os.environ['Path'].split(';') if '' not in paths and '.' not in paths: paths.append('.') try: @@ -737,10 +782,10 @@ def sysfind(cls, name, checker=None, paths=None): except KeyError: pass else: - paths = [re.sub('%SystemRoot%', systemroot, path) + paths = [path.replace('%SystemRoot%', systemroot) for path in paths] else: - paths = py.std.os.environ['PATH'].split(':') + paths = os.environ['PATH'].split(':') tryadd = [] if iswin32: tryadd += os.environ['PATHEXT'].split(os.pathsep) @@ -771,16 +816,18 @@ def _gethomedir(cls): return cls(x) _gethomedir = classmethod(_gethomedir) - #""" - #special class constructors for local filesystem paths - #""" + # """ + # special class constructors for local filesystem paths + # """ + @classmethod def get_temproot(cls): """ return the system's temporary directory (where tempfiles are usually created in) """ - return py.path.local(py.std.tempfile.gettempdir()) - get_temproot = classmethod(get_temproot) + import tempfile + return py.path.local(tempfile.gettempdir()) + @classmethod def mkdtemp(cls, rootdir=None): """ return a Path object pointing to a fresh new temporary directory (which we created ourself). @@ -789,58 +836,43 @@ def mkdtemp(cls, rootdir=None): if rootdir is None: rootdir = cls.get_temproot() return cls(py.error.checked_call(tempfile.mkdtemp, dir=str(rootdir))) - mkdtemp = classmethod(mkdtemp) def make_numbered_dir(cls, prefix='session-', rootdir=None, keep=3, - lock_timeout = 172800): # two days + lock_timeout=172800): # two days """ return unique directory with a number greater than the current maximum one. The number is assumed to start directly after prefix. if keep is true directories with a number less than (maxnum-keep) - will be removed. + will be removed. If .lock files are used (lock_timeout non-zero), + algorithm is multi-process safe. """ if rootdir is None: rootdir = cls.get_temproot() + nprefix = prefix.lower() def parse_num(path): """ parse the number out of a path (if it matches the prefix) """ - bn = path.basename - if bn.startswith(prefix): + nbasename = path.basename.lower() + if nbasename.startswith(nprefix): try: - return int(bn[len(prefix):]) + return int(nbasename[len(nprefix):]) except ValueError: pass - # compute the maximum number currently in use with the - # prefix - lastmax = None - while True: - maxnum = -1 - for path in rootdir.listdir(): - num = parse_num(path) - if num is not None: - maxnum = max(maxnum, num) - - # make the new directory - try: - udir = rootdir.mkdir(prefix + str(maxnum+1)) - except py.error.EEXIST: - # race condition: another thread/process created the dir - # in the meantime. Try counting again - if lastmax == maxnum: - raise - lastmax = maxnum - continue - break - - # put a .lock file in the new directory that will be removed at - # process exit - if lock_timeout: - lockfile = udir.join('.lock') + def create_lockfile(path): + """ exclusively create lockfile. Throws when failed """ mypid = os.getpid() + lockfile = path.join('.lock') if hasattr(lockfile, 'mksymlinkto'): lockfile.mksymlinkto(str(mypid)) else: - lockfile.write(str(mypid)) + fd = py.error.checked_call(os.open, str(lockfile), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) + with os.fdopen(fd, 'w') as f: + f.write(str(mypid)) + return lockfile + + def atexit_remove_lockfile(lockfile): + """ ensure lockfile is removed at process exit """ + mypid = os.getpid() def try_remove_lockfile(): # in a fork() situation, only the last process should # remove the .lock, otherwise the other processes run the @@ -855,19 +887,82 @@ def try_remove_lockfile(): pass atexit.register(try_remove_lockfile) + # compute the maximum number currently in use with the prefix + lastmax = None + while True: + maxnum = -1 + for path in rootdir.listdir(): + num = parse_num(path) + if num is not None: + maxnum = max(maxnum, num) + + # make the new directory + try: + udir = rootdir.mkdir(prefix + str(maxnum+1)) + if lock_timeout: + lockfile = create_lockfile(udir) + atexit_remove_lockfile(lockfile) + except (py.error.EEXIST, py.error.ENOENT, py.error.EBUSY): + # race condition (1): another thread/process created the dir + # in the meantime - try again + # race condition (2): another thread/process spuriously acquired + # lock treating empty directory as candidate + # for removal - try again + # race condition (3): another thread/process tried to create the lock at + # the same time (happened in Python 3.3 on Windows) + # https://ci.appveyor.com/project/pytestbot/py/build/1.0.21/job/ffi85j4c0lqwsfwa + if lastmax == maxnum: + raise + lastmax = maxnum + continue + break + + def get_mtime(path): + """ read file modification time """ + try: + return path.lstat().mtime + except py.error.Error: + pass + + garbage_prefix = prefix + 'garbage-' + + def is_garbage(path): + """ check if path denotes directory scheduled for removal """ + bn = path.basename + return bn.startswith(garbage_prefix) + # prune old directories - if keep: + udir_time = get_mtime(udir) + if keep and udir_time: for path in rootdir.listdir(): num = parse_num(path) if num is not None and num <= (maxnum - keep): - lf = path.join('.lock') try: - t1 = lf.lstat().mtime - t2 = lockfile.lstat().mtime - if not lock_timeout or abs(t2-t1) < lock_timeout: - continue # skip directories still locked - except py.error.Error: - pass # assume that it means that there is no 'lf' + # try acquiring lock to remove directory as exclusive user + if lock_timeout: + create_lockfile(path) + except (py.error.EEXIST, py.error.ENOENT, py.error.EBUSY): + path_time = get_mtime(path) + if not path_time: + # assume directory doesn't exist now + continue + if abs(udir_time - path_time) < lock_timeout: + # assume directory with lockfile exists + # and lock timeout hasn't expired yet + continue + + # path dir locked for exclusive use + # and scheduled for removal to avoid another thread/process + # treating it as a new directory or removal candidate + garbage_path = rootdir.join(garbage_prefix + str(uuid.uuid4())) + try: + path.rename(garbage_path) + garbage_path.remove(rec=1) + except KeyboardInterrupt: + raise + except: # this might be py.error.Error, WindowsError ... + pass + if is_garbage(path): try: path.remove(rec=1) except KeyboardInterrupt: @@ -898,16 +993,22 @@ def try_remove_lockfile(): return udir make_numbered_dir = classmethod(make_numbered_dir) + def copymode(src, dest): """ copy permission from src to dst. """ - py.std.shutil.copymode(src, dest) + import shutil + shutil.copymode(src, dest) + def copystat(src, dest): - """ copy permission, last modification time, last access time, and flags from src to dst.""" - py.std.shutil.copystat(str(src), str(dest)) + """ copy permission, last modification time, + last access time, and flags from src to dst.""" + import shutil + shutil.copystat(str(src), str(dest)) + def copychunked(src, dest): - chunksize = 524288 # half a meg of bytes + chunksize = 524288 # half a meg of bytes fsrc = src.open('rb') try: fdest = dest.open('wb') @@ -922,6 +1023,7 @@ def copychunked(src, dest): finally: fsrc.close() + def isimportable(name): if name and (name[0].isalpha() or name[0] == '_'): name = name.replace("_", '') diff --git a/py/_path/svnwc.py b/py/_path/svnwc.py index 992223c0..b5b9d8d5 100644 --- a/py/_path/svnwc.py +++ b/py/_path/svnwc.py @@ -94,7 +94,7 @@ def _getsvnversion(ver=[]): def _escape_helper(text): text = str(text) - if py.std.sys.platform != 'win32': + if sys.platform != 'win32': text = str(text).replace('$', '\\$') return text @@ -354,7 +354,7 @@ def path_to_fspath(path, addat=True): def url_from_path(path): fspath = path_to_fspath(path, False) - quote = py.std.urllib.quote + from urllib import quote if ISWINDOWS: match = _reg_allow_disk.match(fspath) fspath = fspath.replace('\\', '/') @@ -396,7 +396,7 @@ def makecmdoptions(self): def __str__(self): return "" %(self.username,) -rex_blame = re.compile(r'\s*(\d+)\s*(\S+) (.*)') +rex_blame = re.compile(r'\s*(\d+)\s+(\S+) (.*)') class SvnWCCommandPath(common.PathBase): """ path implementation offering access/modification to svn working copies. @@ -504,7 +504,7 @@ def checkout(self, url=None, rev=None): if url is None: url = self.url if rev is None or rev == -1: - if (py.std.sys.platform != 'win32' and + if (sys.platform != 'win32' and _getsvnversion() == '1.3'): url += "@HEAD" else: @@ -785,7 +785,7 @@ def info(self, usecache=1): info = InfoSvnWCCommand(output) # Can't reliably compare on Windows without access to win32api - if py.std.sys.platform != 'win32': + if sys.platform != 'win32': if info.path != self.localpath: raise py.error.ENOENT(self, "not a versioned resource:" + " %s != %s" % (info.path, self.localpath)) diff --git a/py/_std.py b/py/_std.py index 97a98533..66adb7b0 100644 --- a/py/_std.py +++ b/py/_std.py @@ -1,4 +1,10 @@ import sys +import warnings + + +class PyStdIsDeprecatedWarning(DeprecationWarning): + pass + class Std(object): """ makes top-level python modules available as an attribute, @@ -9,6 +15,9 @@ def __init__(self): self.__dict__ = sys.modules def __getattr__(self, name): + warnings.warn("py.std is deprecated, please import %s directly" % name, + category=PyStdIsDeprecatedWarning, + stacklevel=2) try: m = __import__(name) except ImportError: diff --git a/py/_vendored_packages/__init__.py b/py/_vendored_packages/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/py/_vendored_packages/apipkg-1.5.dist-info/INSTALLER b/py/_vendored_packages/apipkg-1.5.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/py/_vendored_packages/apipkg-1.5.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/py/_vendored_packages/apipkg-1.5.dist-info/METADATA b/py/_vendored_packages/apipkg-1.5.dist-info/METADATA new file mode 100644 index 00000000..ac14b4bb --- /dev/null +++ b/py/_vendored_packages/apipkg-1.5.dist-info/METADATA @@ -0,0 +1,115 @@ +Metadata-Version: 2.1 +Name: apipkg +Version: 1.5 +Summary: apipkg: namespace control and lazy-import mechanism +Home-page: https://github.com/pytest-dev/apipkg +Author: holger krekel +Maintainer: Ronny Pfannschmidt +Maintainer-email: opensource@ronnypfannschmidt.de +License: MIT License +Platform: unix +Platform: linux +Platform: osx +Platform: cygwin +Platform: win32 +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: POSIX +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Topic :: Software Development :: Libraries +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* + +Welcome to apipkg! +------------------------ + +With apipkg you can control the exported namespace of a Python package and +greatly reduce the number of imports for your users. +It is a `small pure Python module`_ that works on CPython 2.7 and 3.4+, +Jython and PyPy. It cooperates well with Python's ``help()`` system, +custom importers (PEP302) and common command-line completion tools. + +Usage is very simple: you can require 'apipkg' as a dependency or you +can copy paste the ~200 lines of code into your project. + + +Tutorial example +------------------- + +Here is a simple ``mypkg`` package that specifies one namespace +and exports two objects imported from different modules:: + + # mypkg/__init__.py + import apipkg + apipkg.initpkg(__name__, { + 'path': { + 'Class1': "_mypkg.somemodule:Class1", + 'clsattr': "_mypkg.othermodule:Class2.attr", + } + } + +The package is initialized with a dictionary as namespace. + +You need to create a ``_mypkg`` package with a ``somemodule.py`` +and ``othermodule.py`` containing the respective classes. +The ``_mypkg`` is not special - it's a completely +regular Python package. + +Namespace dictionaries contain ``name: value`` mappings +where the value may be another namespace dictionary or +a string specifying an import location. On accessing +an namespace attribute an import will be performed:: + + >>> import mypkg + >>> mypkg.path + + >>> mypkg.path.Class1 # '_mypkg.somemodule' gets imported now + + >>> mypkg.path.clsattr # '_mypkg.othermodule' gets imported now + 4 # the value of _mypkg.othermodule.Class2.attr + +The ``mypkg.path`` namespace and its two entries are +loaded when they are accessed. This means: + +* lazy loading - only what is actually needed is ever loaded + +* only the root "mypkg" ever needs to be imported to get + access to the complete functionality + +* the underlying modules are also accessible, for example:: + + from mypkg.sub import Class1 + + +Including apipkg in your package +-------------------------------------- + +If you don't want to add an ``apipkg`` dependency to your package you +can copy the `apipkg.py`_ file somewhere to your own package, +for example ``_mypkg/apipkg.py`` in the above example. You +then import the ``initpkg`` function from that new place and +are good to go. + +.. _`small pure Python module`: +.. _`apipkg.py`: https://github.com/pytest-dev/apipkg/blob/master/src/apipkg/__init__.py + +Feedback? +----------------------- + +If you have questions you are welcome to + +* join the #pylib channel on irc.freenode.net +* create an issue on https://github.com/pytest-dev/apipkg/issues + +have fun, +holger krekel + + diff --git a/py/_vendored_packages/apipkg-1.5.dist-info/RECORD b/py/_vendored_packages/apipkg-1.5.dist-info/RECORD new file mode 100644 index 00000000..8611704e --- /dev/null +++ b/py/_vendored_packages/apipkg-1.5.dist-info/RECORD @@ -0,0 +1,10 @@ +../../../../home/ran/.cache/pycache/tmp/pip-target-oxds71ih/lib/python/apipkg/__init__.cpython-39.pyc,, +../../../../home/ran/.cache/pycache/tmp/pip-target-oxds71ih/lib/python/apipkg/version.cpython-39.pyc,, +apipkg-1.5.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +apipkg-1.5.dist-info/METADATA,sha256=tIG1DSBzSeqmSRpOKHSEBmT1eOPdK8xK01xAIADuks4,3800 +apipkg-1.5.dist-info/RECORD,, +apipkg-1.5.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +apipkg-1.5.dist-info/WHEEL,sha256=gduuPyBvFJQSQ0zdyxF7k0zynDXbIbvg5ZBHoXum5uk,110 +apipkg-1.5.dist-info/top_level.txt,sha256=3TGS6nmN7kjxhUK4LpPCB3QkQI34QYGrT0ZQGWajoZ8,7 +apipkg/__init__.py,sha256=VogR4mDwYmeOdJnjGi-RoMB1qJnD6_puDYj_nRolzhM,6707 +apipkg/version.py,sha256=YN6DnKyEPqjDAauJuwJRG9vlKbWVLd9gAbH7mkQXXNo,114 diff --git a/py/_vendored_packages/apipkg-1.5.dist-info/REQUESTED b/py/_vendored_packages/apipkg-1.5.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/py/_vendored_packages/apipkg-1.5.dist-info/WHEEL b/py/_vendored_packages/apipkg-1.5.dist-info/WHEEL new file mode 100644 index 00000000..1316c41d --- /dev/null +++ b/py/_vendored_packages/apipkg-1.5.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.31.1) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/py/_vendored_packages/apipkg-1.5.dist-info/top_level.txt b/py/_vendored_packages/apipkg-1.5.dist-info/top_level.txt new file mode 100644 index 00000000..e2221c8f --- /dev/null +++ b/py/_vendored_packages/apipkg-1.5.dist-info/top_level.txt @@ -0,0 +1 @@ +apipkg diff --git a/py/_apipkg.py b/py/_vendored_packages/apipkg/__init__.py similarity index 80% rename from py/_apipkg.py rename to py/_vendored_packages/apipkg/__init__.py index a73b8f6d..93180484 100644 --- a/py/_apipkg.py +++ b/py/_vendored_packages/apipkg/__init__.py @@ -1,7 +1,7 @@ """ -apipkg: control the exported namespace of a python package. +apipkg: control the exported namespace of a Python package. -see http://pypi.python.org/pypi/apipkg +see https://pypi.python.org/pypi/apipkg (c) holger krekel, 2009 - MIT license """ @@ -9,7 +9,8 @@ import sys from types import ModuleType -__version__ = '1.3.dev' +from .version import version as __version__ + def _py_abspath(path): """ @@ -22,8 +23,22 @@ def _py_abspath(path): else: return os.path.abspath(path) -def initpkg(pkgname, exportdefs, attr=dict()): + +def distribution_version(name): + """try to get the version of the named distribution, + returs None on failure""" + from pkg_resources import get_distribution, DistributionNotFound + try: + dist = get_distribution(name) + except DistributionNotFound: + pass + else: + return dist.version + + +def initpkg(pkgname, exportdefs, attr=None, eager=False): """ initialize given package from the export definitions. """ + attr = attr or {} oldmod = sys.modules.get(pkgname) d = {} f = getattr(oldmod, '__file__', None) @@ -36,6 +51,8 @@ def initpkg(pkgname, exportdefs, attr=dict()): d['__loader__'] = oldmod.__loader__ if hasattr(oldmod, '__path__'): d['__path__'] = [_py_abspath(p) for p in oldmod.__path__] + if hasattr(oldmod, '__package__'): + d['__package__'] = oldmod.__package__ if '__doc__' not in exportdefs and getattr(oldmod, '__doc__', None): d['__doc__'] = oldmod.__doc__ d.update(attr) @@ -43,8 +60,15 @@ def initpkg(pkgname, exportdefs, attr=dict()): oldmod.__dict__.update(d) mod = ApiModule(pkgname, exportdefs, implprefix=pkgname, attr=d) sys.modules[pkgname] = mod + # eagerload in bypthon to avoid their monkeypatching breaking packages + if 'bpython' in sys.modules or eager: + for module in list(sys.modules.values()): + if isinstance(module, ApiModule): + module.__dict__ + def importobj(modpath, attrname): + """imports a module, then resolves the attrname on it""" module = __import__(modpath, None, None, ['__doc__']) if not attrname: return module @@ -55,13 +79,16 @@ def importobj(modpath, attrname): retval = getattr(retval, x) return retval + class ApiModule(ModuleType): + """the magical lazy-loading module standing""" def __docget(self): try: return self.__doc except AttributeError: if '__doc__' in self.__map__: return self.__makeattr('__doc__') + def __docset(self, value): self.__doc = value __doc__ = property(__docget, __docset) @@ -98,13 +125,13 @@ def __init__(self, name, importspec, implprefix=None, attr=None): self.__map__[name] = (modpath, attrname) def __repr__(self): - l = [] + repr_list = [] if hasattr(self, '__version__'): - l.append("version=" + repr(self.__version__)) + repr_list.append("version=" + repr(self.__version__)) if hasattr(self, '__file__'): - l.append('from ' + repr(self.__file__)) - if l: - return '' % (self.__name__, " ".join(l)) + repr_list.append('from ' + repr(self.__file__)) + if repr_list: + return '' % (self.__name__, " ".join(repr_list)) return '' % (self.__name__,) def __makeattr(self, name): @@ -132,8 +159,10 @@ def __makeattr(self, name): __getattr__ = __makeattr + @property def __dict__(self): - # force all the content of the module to be loaded when __dict__ is read + # force all the content of the module + # to be loaded when __dict__ is read dictdescr = ModuleType.__dict__['__dict__'] dict = dictdescr.__get__(self) if dict is not None: @@ -144,7 +173,6 @@ def __dict__(self): except AttributeError: pass return dict - __dict__ = property(__dict__) def AliasModule(modname, modpath, attrname=None): diff --git a/py/_vendored_packages/apipkg/version.py b/py/_vendored_packages/apipkg/version.py new file mode 100644 index 00000000..c25fc7c9 --- /dev/null +++ b/py/_vendored_packages/apipkg/version.py @@ -0,0 +1,4 @@ +# coding: utf-8 +# file generated by setuptools_scm +# don't change, don't track in version control +version = '1.5' diff --git a/py/_vendored_packages/iniconfig-1.1.1.dist-info/INSTALLER b/py/_vendored_packages/iniconfig-1.1.1.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/py/_vendored_packages/iniconfig-1.1.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/py/_vendored_packages/iniconfig-1.1.1.dist-info/LICENSE b/py/_vendored_packages/iniconfig-1.1.1.dist-info/LICENSE new file mode 100644 index 00000000..31ecdfb1 --- /dev/null +++ b/py/_vendored_packages/iniconfig-1.1.1.dist-info/LICENSE @@ -0,0 +1,19 @@ + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + diff --git a/py/_vendored_packages/iniconfig-1.1.1.dist-info/METADATA b/py/_vendored_packages/iniconfig-1.1.1.dist-info/METADATA new file mode 100644 index 00000000..c078a753 --- /dev/null +++ b/py/_vendored_packages/iniconfig-1.1.1.dist-info/METADATA @@ -0,0 +1,78 @@ +Metadata-Version: 2.1 +Name: iniconfig +Version: 1.1.1 +Summary: iniconfig: brain-dead simple config-ini parsing +Home-page: http://github.com/RonnyPfannschmidt/iniconfig +Author: Ronny Pfannschmidt, Holger Krekel +Author-email: opensource@ronnypfannschmidt.de, holger.krekel@gmail.com +License: MIT License +Platform: unix +Platform: linux +Platform: osx +Platform: cygwin +Platform: win32 +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: POSIX +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: Utilities +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 3 + +iniconfig: brain-dead simple parsing of ini files +======================================================= + +iniconfig is a small and simple INI-file parser module +having a unique set of features: + +* tested against Python2.4 across to Python3.2, Jython, PyPy +* maintains order of sections and entries +* supports multi-line values with or without line-continuations +* supports "#" comments everywhere +* raises errors with proper line-numbers +* no bells and whistles like automatic substitutions +* iniconfig raises an Error if two sections have the same name. + +If you encounter issues or have feature wishes please report them to: + + http://github.com/RonnyPfannschmidt/iniconfig/issues + +Basic Example +=================================== + +If you have an ini file like this:: + + # content of example.ini + [section1] # comment + name1=value1 # comment + name1b=value1,value2 # comment + + [section2] + name2= + line1 + line2 + +then you can do:: + + >>> import iniconfig + >>> ini = iniconfig.IniConfig("example.ini") + >>> ini['section1']['name1'] # raises KeyError if not exists + 'value1' + >>> ini.get('section1', 'name1b', [], lambda x: x.split(",")) + ['value1', 'value2'] + >>> ini.get('section1', 'notexist', [], lambda x: x.split(",")) + [] + >>> [x.name for x in list(ini)] + ['section1', 'section2'] + >>> list(list(ini)[0].items()) + [('name1', 'value1'), ('name1b', 'value1,value2')] + >>> 'section1' in ini + True + >>> 'inexistendsection' in ini + False + + diff --git a/py/_vendored_packages/iniconfig-1.1.1.dist-info/RECORD b/py/_vendored_packages/iniconfig-1.1.1.dist-info/RECORD new file mode 100644 index 00000000..73a6fe1e --- /dev/null +++ b/py/_vendored_packages/iniconfig-1.1.1.dist-info/RECORD @@ -0,0 +1,11 @@ +../../../../home/ran/.cache/pycache/tmp/pip-target-oxds71ih/lib/python/iniconfig/__init__.cpython-39.pyc,, +iniconfig-1.1.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +iniconfig-1.1.1.dist-info/LICENSE,sha256=KvaAw570k_uCgwNW0dPfGstaBgM8ui3sehniHKp3qGY,1061 +iniconfig-1.1.1.dist-info/METADATA,sha256=_4-oFKpRXuZv5rzepScpXRwhq6DzqsgbnA5ZpgMUMcs,2405 +iniconfig-1.1.1.dist-info/RECORD,, +iniconfig-1.1.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +iniconfig-1.1.1.dist-info/WHEEL,sha256=ADKeyaGyKF5DwBNE0sRE5pvW-bSkFMJfBuhzZ3rceP4,110 +iniconfig-1.1.1.dist-info/top_level.txt,sha256=7KfM0fugdlToj9UW7enKXk2HYALQD8qHiyKtjhSzgN8,10 +iniconfig/__init__.py,sha256=-pBe5AF_6aAwo1CxJQ8i_zJq6ejc6IxHta7qk2tNJhY,5208 +iniconfig/__init__.pyi,sha256=-4KOctzq28ohRmTZsqlH6aylyFqsNKxYqtk1dteypi4,1205 +iniconfig/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/py/_vendored_packages/iniconfig-1.1.1.dist-info/REQUESTED b/py/_vendored_packages/iniconfig-1.1.1.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/py/_vendored_packages/iniconfig-1.1.1.dist-info/WHEEL b/py/_vendored_packages/iniconfig-1.1.1.dist-info/WHEEL new file mode 100644 index 00000000..6d38aa06 --- /dev/null +++ b/py/_vendored_packages/iniconfig-1.1.1.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.35.1) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/py/_vendored_packages/iniconfig-1.1.1.dist-info/top_level.txt b/py/_vendored_packages/iniconfig-1.1.1.dist-info/top_level.txt new file mode 100644 index 00000000..9dda5369 --- /dev/null +++ b/py/_vendored_packages/iniconfig-1.1.1.dist-info/top_level.txt @@ -0,0 +1 @@ +iniconfig diff --git a/py/_iniconfig.py b/py/_vendored_packages/iniconfig/__init__.py similarity index 92% rename from py/_iniconfig.py rename to py/_vendored_packages/iniconfig/__init__.py index 92b50bd8..6ad9eaf8 100644 --- a/py/_iniconfig.py +++ b/py/_vendored_packages/iniconfig/__init__.py @@ -1,12 +1,11 @@ """ brain-dead simple parser for ini-style files. (C) Ronny Pfannschmidt, Holger Krekel -- MIT licensed """ -__version__ = "0.2.dev2" - __all__ = ['IniConfig', 'ParseError'] COMMENTCHARS = "#;" + class ParseError(Exception): def __init__(self, path, lineno, msg): Exception.__init__(self, path, lineno, msg) @@ -15,7 +14,8 @@ def __init__(self, path, lineno, msg): self.msg = msg def __str__(self): - return "%s:%s: %s" %(self.path, self.lineno+1, self.msg) + return "%s:%s: %s" % (self.path, self.lineno+1, self.msg) + class SectionWrapper(object): def __init__(self, config, name): @@ -26,13 +26,15 @@ def lineof(self, name): return self.config.lineof(self.name, name) def get(self, key, default=None, convert=str): - return self.config.get(self.name, key, convert=convert, default=default) + return self.config.get(self.name, key, + convert=convert, default=default) def __getitem__(self, key): return self.config.sections[self.name][key] def __iter__(self): section = self.config.sections.get(self.name, []) + def lineof(key): return self.config.lineof(self.name, key) for name in sorted(section, key=lineof): @@ -45,7 +47,7 @@ def items(self): class IniConfig(object): def __init__(self, path, data=None): - self.path = str(path) # convenience + self.path = str(path) # convenience if data is None: f = open(self.path) try: @@ -64,11 +66,11 @@ def __init__(self, path, data=None): self._sources[section, name] = lineno if name is None: if section in self.sections: - self._raise(lineno, 'duplicate section %r'%(section, )) + self._raise(lineno, 'duplicate section %r' % (section, )) self.sections[section] = {} else: if name in self.sections[section]: - self._raise(lineno, 'duplicate name %r'%(name, )) + self._raise(lineno, 'duplicate name %r' % (name, )) self.sections[section][name] = value def _raise(self, lineno, msg): @@ -157,6 +159,7 @@ def __iter__(self): def __contains__(self, arg): return arg in self.sections + def iscommentline(line): c = line.lstrip()[:1] return c in COMMENTCHARS diff --git a/py/_vendored_packages/iniconfig/__init__.pyi b/py/_vendored_packages/iniconfig/__init__.pyi new file mode 100644 index 00000000..b6284bec --- /dev/null +++ b/py/_vendored_packages/iniconfig/__init__.pyi @@ -0,0 +1,31 @@ +from typing import Callable, Iterator, Mapping, Optional, Tuple, TypeVar, Union +from typing_extensions import Final + +_D = TypeVar('_D') +_T = TypeVar('_T') + +class ParseError(Exception): + # Private __init__. + path: Final[str] + lineno: Final[int] + msg: Final[str] + +class SectionWrapper: + # Private __init__. + config: Final[IniConfig] + name: Final[str] + def __getitem__(self, key: str) -> str: ... + def __iter__(self) -> Iterator[str]: ... + def get(self, key: str, default: _D = ..., convert: Callable[[str], _T] = ...) -> Union[_T, _D]: ... + def items(self) -> Iterator[Tuple[str, str]]: ... + def lineof(self, name: str) -> Optional[int]: ... + +class IniConfig: + path: Final[str] + sections: Final[Mapping[str, Mapping[str, str]]] + def __init__(self, path: str, data: Optional[str] = None): ... + def __contains__(self, arg: str) -> bool: ... + def __getitem__(self, name: str) -> SectionWrapper: ... + def __iter__(self) -> Iterator[SectionWrapper]: ... + def get(self, section: str, name: str, default: _D = ..., convert: Callable[[str], _T] = ...) -> Union[_T, _D]: ... + def lineof(self, section: str, name: Optional[str] = ...) -> Optional[int]: ... diff --git a/py/_vendored_packages/iniconfig/py.typed b/py/_vendored_packages/iniconfig/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/py/error.pyi b/py/error.pyi new file mode 100644 index 00000000..034eba60 --- /dev/null +++ b/py/error.pyi @@ -0,0 +1,129 @@ +from typing import Any, Callable, TypeVar + +_T = TypeVar('_T') + +def checked_call(func: Callable[..., _T], *args: Any, **kwargs: Any) -> _T: ... +class Error(EnvironmentError): ... +class EPERM(Error): ... +class ENOENT(Error): ... +class ESRCH(Error): ... +class EINTR(Error): ... +class EIO(Error): ... +class ENXIO(Error): ... +class E2BIG(Error): ... +class ENOEXEC(Error): ... +class EBADF(Error): ... +class ECHILD(Error): ... +class EAGAIN(Error): ... +class ENOMEM(Error): ... +class EACCES(Error): ... +class EFAULT(Error): ... +class ENOTBLK(Error): ... +class EBUSY(Error): ... +class EEXIST(Error): ... +class EXDEV(Error): ... +class ENODEV(Error): ... +class ENOTDIR(Error): ... +class EISDIR(Error): ... +class EINVAL(Error): ... +class ENFILE(Error): ... +class EMFILE(Error): ... +class ENOTTY(Error): ... +class ETXTBSY(Error): ... +class EFBIG(Error): ... +class ENOSPC(Error): ... +class ESPIPE(Error): ... +class EROFS(Error): ... +class EMLINK(Error): ... +class EPIPE(Error): ... +class EDOM(Error): ... +class ERANGE(Error): ... +class EDEADLCK(Error): ... +class ENAMETOOLONG(Error): ... +class ENOLCK(Error): ... +class ENOSYS(Error): ... +class ENOTEMPTY(Error): ... +class ELOOP(Error): ... +class EWOULDBLOCK(Error): ... +class ENOMSG(Error): ... +class EIDRM(Error): ... +class ECHRNG(Error): ... +class EL2NSYNC(Error): ... +class EL3HLT(Error): ... +class EL3RST(Error): ... +class ELNRNG(Error): ... +class EUNATCH(Error): ... +class ENOCSI(Error): ... +class EL2HLT(Error): ... +class EBADE(Error): ... +class EBADR(Error): ... +class EXFULL(Error): ... +class ENOANO(Error): ... +class EBADRQC(Error): ... +class EBADSLT(Error): ... +class EDEADLOCK(Error): ... +class EBFONT(Error): ... +class ENOSTR(Error): ... +class ENODATA(Error): ... +class ETIME(Error): ... +class ENOSR(Error): ... +class ENONET(Error): ... +class ENOPKG(Error): ... +class EREMOTE(Error): ... +class ENOLINK(Error): ... +class EADV(Error): ... +class ESRMNT(Error): ... +class ECOMM(Error): ... +class EPROTO(Error): ... +class EMULTIHOP(Error): ... +class EDOTDOT(Error): ... +class EBADMSG(Error): ... +class EOVERFLOW(Error): ... +class ENOTUNIQ(Error): ... +class EBADFD(Error): ... +class EREMCHG(Error): ... +class ELIBACC(Error): ... +class ELIBBAD(Error): ... +class ELIBSCN(Error): ... +class ELIBMAX(Error): ... +class ELIBEXEC(Error): ... +class EILSEQ(Error): ... +class ERESTART(Error): ... +class ESTRPIPE(Error): ... +class EUSERS(Error): ... +class ENOTSOCK(Error): ... +class EDESTADDRREQ(Error): ... +class EMSGSIZE(Error): ... +class EPROTOTYPE(Error): ... +class ENOPROTOOPT(Error): ... +class EPROTONOSUPPORT(Error): ... +class ESOCKTNOSUPPORT(Error): ... +class ENOTSUP(Error): ... +class EOPNOTSUPP(Error): ... +class EPFNOSUPPORT(Error): ... +class EAFNOSUPPORT(Error): ... +class EADDRINUSE(Error): ... +class EADDRNOTAVAIL(Error): ... +class ENETDOWN(Error): ... +class ENETUNREACH(Error): ... +class ENETRESET(Error): ... +class ECONNABORTED(Error): ... +class ECONNRESET(Error): ... +class ENOBUFS(Error): ... +class EISCONN(Error): ... +class ENOTCONN(Error): ... +class ESHUTDOWN(Error): ... +class ETOOMANYREFS(Error): ... +class ETIMEDOUT(Error): ... +class ECONNREFUSED(Error): ... +class EHOSTDOWN(Error): ... +class EHOSTUNREACH(Error): ... +class EALREADY(Error): ... +class EINPROGRESS(Error): ... +class ESTALE(Error): ... +class EUCLEAN(Error): ... +class ENOTNAM(Error): ... +class ENAVAIL(Error): ... +class EISNAM(Error): ... +class EREMOTEIO(Error): ... +class EDQUOT(Error): ... diff --git a/py/iniconfig.pyi b/py/iniconfig.pyi new file mode 100644 index 00000000..b6284bec --- /dev/null +++ b/py/iniconfig.pyi @@ -0,0 +1,31 @@ +from typing import Callable, Iterator, Mapping, Optional, Tuple, TypeVar, Union +from typing_extensions import Final + +_D = TypeVar('_D') +_T = TypeVar('_T') + +class ParseError(Exception): + # Private __init__. + path: Final[str] + lineno: Final[int] + msg: Final[str] + +class SectionWrapper: + # Private __init__. + config: Final[IniConfig] + name: Final[str] + def __getitem__(self, key: str) -> str: ... + def __iter__(self) -> Iterator[str]: ... + def get(self, key: str, default: _D = ..., convert: Callable[[str], _T] = ...) -> Union[_T, _D]: ... + def items(self) -> Iterator[Tuple[str, str]]: ... + def lineof(self, name: str) -> Optional[int]: ... + +class IniConfig: + path: Final[str] + sections: Final[Mapping[str, Mapping[str, str]]] + def __init__(self, path: str, data: Optional[str] = None): ... + def __contains__(self, arg: str) -> bool: ... + def __getitem__(self, name: str) -> SectionWrapper: ... + def __iter__(self) -> Iterator[SectionWrapper]: ... + def get(self, section: str, name: str, default: _D = ..., convert: Callable[[str], _T] = ...) -> Union[_T, _D]: ... + def lineof(self, section: str, name: Optional[str] = ...) -> Optional[int]: ... diff --git a/py/io.pyi b/py/io.pyi new file mode 100644 index 00000000..d377e240 --- /dev/null +++ b/py/io.pyi @@ -0,0 +1,130 @@ +from io import StringIO as TextIO +from io import BytesIO as BytesIO +from typing import Any, AnyStr, Callable, Generic, IO, List, Optional, Text, Tuple, TypeVar, Union, overload +from typing_extensions import Final +import sys + +_T = TypeVar("_T") + +class FDCapture(Generic[AnyStr]): + def __init__(self, targetfd: int, tmpfile: Optional[IO[AnyStr]] = ..., now: bool = ..., patchsys: bool = ...) -> None: ... + def start(self) -> None: ... + def done(self) -> IO[AnyStr]: ... + def writeorg(self, data: AnyStr) -> None: ... + +class StdCaptureFD: + def __init__( + self, + out: Union[bool, IO[str]] = ..., + err: Union[bool, IO[str]] = ..., + mixed: bool = ..., + in_: bool = ..., + patchsys: bool = ..., + now: bool = ..., + ) -> None: ... + @classmethod + def call(cls, func: Callable[..., _T], *args: Any, **kwargs: Any) -> Tuple[_T, str, str]: ... + def reset(self) -> Tuple[str, str]: ... + def suspend(self) -> Tuple[str, str]: ... + def startall(self) -> None: ... + def resume(self) -> None: ... + def done(self, save: bool = ...) -> Tuple[IO[str], IO[str]]: ... + def readouterr(self) -> Tuple[str, str]: ... + +class StdCapture: + def __init__( + self, + out: Union[bool, IO[str]] = ..., + err: Union[bool, IO[str]] = ..., + in_: bool = ..., + mixed: bool = ..., + now: bool = ..., + ) -> None: ... + @classmethod + def call(cls, func: Callable[..., _T], *args: Any, **kwargs: Any) -> Tuple[_T, str, str]: ... + def reset(self) -> Tuple[str, str]: ... + def suspend(self) -> Tuple[str, str]: ... + def startall(self) -> None: ... + def resume(self) -> None: ... + def done(self, save: bool = ...) -> Tuple[IO[str], IO[str]]: ... + def readouterr(self) -> Tuple[IO[str], IO[str]]: ... + +# XXX: The type here is not exactly right. If f is IO[bytes] and +# encoding is not None, returns some weird hybrid, not exactly IO[bytes]. +def dupfile( + f: IO[AnyStr], + mode: Optional[str] = ..., + buffering: int = ..., + raising: bool = ..., + encoding: Optional[str] = ..., +) -> IO[AnyStr]: ... +def get_terminal_width() -> int: ... +def ansi_print( + text: Union[str, Text], + esc: Union[Union[str, Text], Tuple[Union[str, Text], ...]], + file: Optional[IO[Any]] = ..., + newline: bool = ..., + flush: bool = ..., +) -> None: ... +def saferepr(obj, maxsize: int = ...) -> str: ... + +class TerminalWriter: + stringio: TextIO + encoding: Final[str] + hasmarkup: bool + def __init__(self, file: Optional[IO[str]] = ..., stringio: bool = ..., encoding: Optional[str] = ...) -> None: ... + @property + def fullwidth(self) -> int: ... + @fullwidth.setter + def fullwidth(self, value: int) -> None: ... + @property + def chars_on_current_line(self) -> int: ... + @property + def width_of_current_line(self) -> int: ... + def markup( + self, + text: str, + *, + black: int = ..., red: int = ..., green: int = ..., yellow: int = ..., blue: int = ..., purple: int = ..., + cyan: int = ..., white: int = ..., Black: int = ..., Red: int = ..., Green: int = ..., Yellow: int = ..., + Blue: int = ..., Purple: int = ..., Cyan: int = ..., White: int = ..., bold: int = ..., light: int = ..., + blink: int = ..., invert: int = ..., + ) -> str: ... + def sep( + self, + sepchar: str, + title: Optional[str] = ..., + fullwidth: Optional[int] = ..., + *, + black: int = ..., red: int = ..., green: int = ..., yellow: int = ..., blue: int = ..., purple: int = ..., + cyan: int = ..., white: int = ..., Black: int = ..., Red: int = ..., Green: int = ..., Yellow: int = ..., + Blue: int = ..., Purple: int = ..., Cyan: int = ..., White: int = ..., bold: int = ..., light: int = ..., + blink: int = ..., invert: int = ..., + ) -> None: ... + def write( + self, + msg: str, + *, + black: int = ..., red: int = ..., green: int = ..., yellow: int = ..., blue: int = ..., purple: int = ..., + cyan: int = ..., white: int = ..., Black: int = ..., Red: int = ..., Green: int = ..., Yellow: int = ..., + Blue: int = ..., Purple: int = ..., Cyan: int = ..., White: int = ..., bold: int = ..., light: int = ..., + blink: int = ..., invert: int = ..., + ) -> None: ... + def line( + self, + s: str = ..., + *, + black: int = ..., red: int = ..., green: int = ..., yellow: int = ..., blue: int = ..., purple: int = ..., + cyan: int = ..., white: int = ..., Black: int = ..., Red: int = ..., Green: int = ..., Yellow: int = ..., + Blue: int = ..., Purple: int = ..., Cyan: int = ..., White: int = ..., bold: int = ..., light: int = ..., + blink: int = ..., invert: int = ..., + ) -> None: ... + def reline( + self, + line: str, + *, + black: int = ..., red: int = ..., green: int = ..., yellow: int = ..., blue: int = ..., purple: int = ..., + cyan: int = ..., white: int = ..., Black: int = ..., Red: int = ..., Green: int = ..., Yellow: int = ..., + Blue: int = ..., Purple: int = ..., Cyan: int = ..., White: int = ..., bold: int = ..., light: int = ..., + blink: int = ..., invert: int = ..., + ) -> None: ... diff --git a/py/path.pyi b/py/path.pyi new file mode 100644 index 00000000..1ddab960 --- /dev/null +++ b/py/path.pyi @@ -0,0 +1,197 @@ +from typing import Any, AnyStr, Callable, ContextManager, Generic, IO, Iterable, Iterator, List, Optional, Text, Type, Union +from typing_extensions import Final, Literal +import os +import sys + +class _FNMatcher(Generic[AnyStr]): + pattern: AnyStr = ... + def __init__(self, pattern: AnyStr) -> None: ... + def __call__(self, path: local) -> bool: ... + +class _Stat: + path: Final[local] = ... + mode: Final[int] + ino: Final[int] + dev: Final[int] + nlink: Final[int] + uid: Final[int] + gid: Final[int] + size: Final[int] + atime: Final[float] + mtime: Final[float] + ctime: Final[float] + atime_ns: Final[int] + mtime_ns: Final[int] + ctime_ns: Final[int] + if sys.version_info >= (3, 8) and sys.platform == "win32": + reparse_tag: Final[int] + blocks: Final[int] + blksize: Final[int] + rdev: Final[int] + flags: Final[int] + gen: Final[int] + birthtime: Final[int] + rsize: Final[int] + creator: Final[int] + type: Final[int] + if sys.platform != 'win32': + @property + def owner(self) -> str: ... + @property + def group(self) -> str: ... + def isdir(self) -> bool: ... + def isfile(self) -> bool: ... + def islink(self) -> bool: ... + + +if sys.version_info >= (3, 6): + _PathLike = os.PathLike +else: + class _PathLike(Generic[AnyStr]): + def __fspath__(self) -> AnyStr: ... +_PathType = Union[bytes, Text, _PathLike[str], _PathLike[bytes], local] + +class local(_PathLike[str]): + class ImportMismatchError(ImportError): ... + + sep: Final[str] + strpath: Final[str] + + def __init__(self, path: _PathType = ..., expanduser: bool = ...) -> None: ... + def __hash__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + def __lt__(self, other: object) -> bool: ... + def __gt__(self, other: object) -> bool: ... + def __add__(self, other: object) -> local: ... + def __cmp__(self, other: object) -> int: ... + def __div__(self, other: _PathType) -> local: ... + def __truediv__(self, other: _PathType) -> local: ... + def __fspath__(self) -> str: ... + + @classmethod + def get_temproot(cls) -> local: ... + @classmethod + def make_numbered_dir( + cls, + prefix: str = ..., + rootdir: Optional[local] = ..., + keep: Optional[int] = ..., + lock_timeout: int = ..., + ) -> local: ... + @classmethod + def mkdtemp(cls, rootdir: Optional[local] = ...) -> local: ... + @classmethod + def sysfind( + cls, + name: _PathType, + checker: Optional[Callable[[local], bool]] = ..., + paths: Optional[Iterable[_PathType]] = ..., + ) -> Optional[local]: ... + + @property + def basename(self) -> str: ... + @property + def dirname(self) -> str: ... + @property + def purebasename(self) -> str: ... + @property + def ext(self) -> str: ... + + def as_cwd(self) -> ContextManager[Optional[local]]: ... + def atime(self) -> float: ... + def bestrelpath(self, dest: local) -> str: ... + def chdir(self) -> local: ... + def check( + self, + *, + basename: int = ..., notbasename: int = ..., + basestarts: int = ..., notbasestarts: int = ..., + dir: int = ..., notdir: int = ..., + dotfile: int = ..., notdotfile: int = ..., + endswith: int = ..., notendswith: int = ..., + exists: int = ..., notexists: int = ..., + ext: int = ..., notext: int = ..., + file: int = ..., notfile: int = ..., + fnmatch: int = ..., notfnmatch: int = ..., + link: int = ..., notlink: int = ..., + relto: int = ..., notrelto: int = ..., + ) -> bool: ... + def chmod(self, mode: int, rec: Union[int, str, Text, Callable[[local], bool]] = ...) -> None: ... + if sys.platform != 'win32': + def chown(self, user: Union[int, str], group: Union[int, str], rec: int = ...) -> None: ... + def common(self, other: local) -> Optional[local]: ... + def computehash(self, hashtype: str = ..., chunksize: int = ...) -> str: ... + def copy(self, target: local, mode: bool = ..., stat: bool = ...) -> None: ... + def dirpath(self, *args: _PathType, abs: int = ...) -> local: ... + def dump(self, obj: Any, bin: Optional[int] = ...) -> None: ... + def ensure(self, *args: _PathType, dir: int = ...) -> local: ... + def ensure_dir(self, *args: _PathType) -> local: ... + def exists(self) -> bool: ... + def fnmatch(self, pattern: str): _FNMatcher + def isdir(self) -> bool: ... + def isfile(self) -> bool: ... + def islink(self) -> bool: ... + def join(self, *args: _PathType, abs: int = ...) -> local: ... + def listdir( + self, + fil: Optional[Union[str, Text, Callable[[local], bool]]] = ..., + sort: Optional[bool] = ..., + ) -> List[local]: ... + def load(self) -> Any: ... + def lstat(self) -> _Stat: ... + def mkdir(self, *args: _PathType) -> local: ... + if sys.platform != 'win32': + def mklinkto(self, oldname: Union[str, local]) -> None: ... + def mksymlinkto(self, value: local, absolute: int = ...) -> None: ... + def move(self, target: local) -> None: ... + def mtime(self) -> float: ... + def new( + self, + *, + drive: str = ..., + dirname: str = ..., + basename: str = ..., + purebasename: str = ..., + ext: str = ..., + ) -> local: ... + def open(self, mode: str = ..., ensure: bool = ..., encoding: Optional[str] = ...) -> IO[Any]: ... + def parts(self, reverse: bool = ...) -> List[local]: ... + def pyimport( + self, + modname: Optional[str] = ..., + ensuresyspath: Union[bool, Literal["append", "importlib"]] = ..., + ) -> Any: ... + def pypkgpath(self) -> Optional[local]: ... + def read(self, mode: str = ...) -> Union[Text, bytes]: ... + def read_binary(self) -> bytes: ... + def read_text(self, encoding: str) -> Text: ... + def readlines(self, cr: int = ...) -> List[str]: ... + if sys.platform != 'win32': + def readlink(self) -> str: ... + def realpath(self) -> local: ... + def relto(self, relpath: Union[str, local]) -> str: ... + def remove(self, rec: int = ..., ignore_errors: bool = ...) -> None: ... + def rename(self, target: _PathType) -> None: ... + def samefile(self, other: _PathType) -> bool: ... + def setmtime(self, mtime: Optional[float] = ...) -> None: ... + def size(self) -> int: ... + def stat(self, raising: bool = ...) -> _Stat: ... + def sysexec(self, *argv: Any, **popen_opts: Any) -> Text: ... + def visit( + self, + fil: Optional[Union[str, Text, Callable[[local], bool]]] = ..., + rec: Optional[Union[Literal[1, True], str, Text, Callable[[local], bool]]] = ..., + ignore: Type[Exception] = ..., + bf: bool = ..., + sort: bool = ..., + ) -> Iterator[local]: ... + def write(self, data: Any, mode: str = ..., ensure: bool = ...) -> None: ... + def write_binary(self, data: bytes, ensure: bool = ...) -> None: ... + def write_text(self, data: Union[str, Text], encoding: str, ensure: bool = ...) -> None: ... + + +# Untyped types below here. +svnwc: Any +svnurl: Any +SvnAuth: Any diff --git a/py/py.typed b/py/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/py/xml.pyi b/py/xml.pyi new file mode 100644 index 00000000..9c44480a --- /dev/null +++ b/py/xml.pyi @@ -0,0 +1,25 @@ +from typing import ClassVar, Generic, Iterable, Text, Type, Union +from typing_extensions import Final + +class raw: + uniobj: Final[Text] + def __init__(self, uniobj: Text) -> None: ... + +class _NamespaceMetaclass(type): + def __getattr__(self, name: str) -> Type[Tag]: ... + +class Namespace(metaclass=_NamespaceMetaclass): ... + +class Tag(list): + class Attr: + def __getattr__(self, attr: str) -> Text: ... + attr: Final[Attr] + def __init__(self, *args: Union[Text, raw, Tag, Iterable[Tag]], **kwargs: Union[Text, raw]) -> None: ... + def unicode(self, indent: int = ...) -> Text: ... + +class html(Namespace): + class Style: + def __init__(self, **kw: Union[str, Text]) -> None: ... + style: ClassVar[Style] + +def escape(ustring: Union[str, Text]) -> Text: ... diff --git a/setup.py b/setup.py index f21230f9..d097daa5 100644 --- a/setup.py +++ b/setup.py @@ -1,17 +1,4 @@ -import os -import sys - -from setuptools import setup - - -def get_version(): - p = os.path.join(os.path.dirname( - os.path.abspath(__file__)), "py", "__init__.py") - with open(p) as f: - for line in f.readlines(): - if "__version__" in line: - return line.strip().split("=")[-1].strip(" '") - raise ValueError("could not read version") +from setuptools import setup, find_packages def main(): @@ -19,10 +6,12 @@ def main(): name='py', description='library with cross-python path, ini-parsing, io, code, log facilities', long_description=open('README.rst').read(), - version=get_version(), - url='http://py.readthedocs.io/', + use_scm_version={"write_to": "py/_version.py"}, + setup_requires=["setuptools-scm"], + url='https://py.readthedocs.io/', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', author='holger krekel, Ronny Pfannschmidt, Benjamin Peterson and others', author_email='pytest-dev@python.org', classifiers=['Development Status :: 6 - Mature', @@ -35,15 +24,21 @@ def main(): 'Topic :: Software Development :: Libraries', 'Topic :: Utilities', 'Programming Language :: Python', - 'Programming Language :: Python :: 3'], - packages=['py', - 'py._code', - 'py._io', - 'py._log', - 'py._path', - 'py._process', - ], + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + ], + packages=find_packages(exclude=['tasks', 'testing']), + include_package_data=True, zip_safe=False, + package_data={ + "": ["py.typed"], + }, ) if __name__ == '__main__': diff --git a/tasks/__init__.py b/tasks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tasks/vendoring.py b/tasks/vendoring.py new file mode 100644 index 00000000..3c7d6015 --- /dev/null +++ b/tasks/vendoring.py @@ -0,0 +1,41 @@ +from __future__ import absolute_import, print_function +import os.path +import shutil +import subprocess +import sys + +VENDOR_TARGET = "py/_vendored_packages" +GOOD_FILES = ('README.md', '__init__.py') + + +def remove_libs(): + print("removing vendored libs") + for filename in os.listdir(VENDOR_TARGET): + if filename not in GOOD_FILES: + path = os.path.join(VENDOR_TARGET, filename) + print(" ", path) + if os.path.isfile(path): + os.remove(path) + else: + shutil.rmtree(path) + + +def update_libs(): + print("installing libs") + subprocess.check_call(( + sys.executable, '-m', 'pip', 'install', + '--target', VENDOR_TARGET, 'apipkg', 'iniconfig', + )) + subprocess.check_call(('git', 'add', VENDOR_TARGET)) + print("Please commit to finish the update after running the tests:") + print() + print(' git commit -am "Updated vendored libs"') + + +def main(): + remove_libs() + update_libs() + + +if __name__ == '__main__': + exit(main()) diff --git a/testing/code/test_assertion.py b/testing/code/test_assertion.py index 47658c31..e2a7f903 100644 --- a/testing/code/test_assertion.py +++ b/testing/code/test_assertion.py @@ -1,7 +1,9 @@ import pytest, py +import re def exvalue(): - return py.std.sys.exc_info()[1] + import sys + return sys.exc_info()[1] def f(): return 2 @@ -23,13 +25,7 @@ def test_assert_within_finally(): i = 42 """) s = excinfo.exconly() - assert py.std.re.search("division.+by zero", s) is not None - - #def g(): - # A.f() - #excinfo = getexcinfo(TypeError, g) - #msg = getmsg(excinfo) - #assert msg.find("must be called with A") != -1 + assert re.search("ZeroDivisionError:.*division", s) is not None def test_assert_multiline_1(): @@ -67,7 +63,6 @@ def test_is(): assert s.startswith("assert 1 is 2") -@py.test.mark.skipif("sys.version_info < (2,6)") def test_attrib(): class Foo(object): b = 1 @@ -79,7 +74,6 @@ class Foo(object): s = str(e) assert s.startswith("assert 1 == 2") -@py.test.mark.skipif("sys.version_info < (2,6)") def test_attrib_inst(): class Foo(object): b = 1 @@ -141,7 +135,10 @@ def test_assert_implicit_multiline(): e = exvalue() assert str(e).find('assert [1, 2, 3] !=') != -1 - +@py.test.mark.xfail(py.test.__version__[0] != "2", + reason="broken on modern pytest", + run=False +) def test_assert_with_brokenrepr_arg(): class BrokenRepr: def __repr__(self): 0 / 0 @@ -228,7 +225,6 @@ def test_underscore_api(): py.code._reinterpret_old # used by pypy py.code._reinterpret -@py.test.mark.skipif("sys.version_info < (2,6)") def test_assert_customizable_reprcompare(monkeypatch): util = pytest.importorskip("_pytest.assertion.util") monkeypatch.setattr(util, '_reprcompare', lambda *args: 'hello') @@ -277,8 +273,9 @@ def test_hello(): "*1 failed*", ]) - -@pytest.mark.skipif("sys.version_info < (2,5)") +@py.test.mark.xfail(py.test.__version__[0] != "2", + reason="broken on modern pytest", + run=False) def test_assert_raise_subclass(): class SomeEx(AssertionError): def __init__(self, *args): diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 8f19f659..c148ab8c 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1,11 +1,14 @@ # -*- coding: utf-8 -*- import py +import pytest +import sys +from test_source import astonly + from py._code.code import FormattedExcinfo, ReprExceptionInfo queue = py.builtin._tryimport('queue', 'Queue') failsonjython = py.test.mark.xfail("sys.platform.startswith('java')") -from test_source import astonly try: import importlib @@ -14,21 +17,32 @@ else: invalidate_import_caches = getattr(importlib, "invalidate_caches", None) -import pytest + pytest_version_info = tuple(map(int, pytest.__version__.split(".")[:3])) +broken_on_modern_pytest = pytest.mark.xfail( + pytest_version_info[0] != 2, + reason="this test hasn't been fixed after moving py.code into pytest", + run=False + ) + + class TWMock: def __init__(self): self.lines = [] + def sep(self, sep, line=None): self.lines.append((sep, line)) + def line(self, line, **kw): self.lines.append(line) + def markup(self, text, **kw): return text fullwidth = 80 + def test_excinfo_simple(): try: raise ValueError @@ -36,18 +50,22 @@ def test_excinfo_simple(): info = py.code.ExceptionInfo() assert info.type == ValueError + def test_excinfo_getstatement(): def g(): raise ValueError + def f(): g() try: f() except ValueError: excinfo = py.code.ExceptionInfo() - linenumbers = [py.code.getrawcode(f).co_firstlineno-1+3, - py.code.getrawcode(f).co_firstlineno-1+1, - py.code.getrawcode(g).co_firstlineno-1+1,] + linenumbers = [ + py.code.getrawcode(f).co_firstlineno-1+3, + py.code.getrawcode(f).co_firstlineno-1+1, + py.code.getrawcode(g).co_firstlineno-1+1, + ] l = list(excinfo.traceback) foundlinenumbers = [x.lineno for x in l] assert foundlinenumbers == linenumbers @@ -92,7 +110,7 @@ def test_traceback_entries(self): def test_traceback_entry_getsource(self): tb = self.excinfo.traceback - s = str(tb[-1].getsource() ) + s = str(tb[-1].getsource()) assert s.startswith("def f():") assert s.endswith("raise ValueError") @@ -164,10 +182,12 @@ def f(n): def test_traceback_no_recursion_index(self): def do_stuff(): raise RuntimeError + def reraise_me(): import sys exc, val, tb = sys.exc_info() py.builtin._reraise(exc, val, tb) + def f(n): try: do_stuff() @@ -179,7 +199,7 @@ def f(n): assert recindex is None def test_traceback_messy_recursion(self): - #XXX: simplified locally testable version + # XXX: simplified locally testable version decorator = py.test.importorskip('decorator').decorator def log(f, *k, **kw): @@ -195,17 +215,18 @@ def fail(): excinfo = py.test.raises(ValueError, fail) assert excinfo.traceback.recursionindex() is None - - def test_traceback_getcrashentry(self): def i(): __tracebackhide__ = True raise ValueError + def h(): i() + def g(): __tracebackhide__ = True h() + def f(): g() @@ -221,6 +242,7 @@ def test_traceback_getcrashentry_empty(self): def g(): __tracebackhide__ = True raise ValueError + def f(): __tracebackhide__ = True g() @@ -233,9 +255,11 @@ def f(): assert entry.lineno == co.firstlineno + 2 assert entry.frame.code.name == 'g' + def hello(x): x + 5 + def test_tbentry_reinterpret(): try: hello("hello") @@ -245,6 +269,7 @@ def test_tbentry_reinterpret(): msg = tbentry.reinterpret() assert msg.startswith("TypeError: ('hello' + 5)") + def test_excinfo_exconly(): excinfo = py.test.raises(ValueError, h) assert excinfo.exconly().startswith('ValueError') @@ -254,11 +279,13 @@ def test_excinfo_exconly(): assert msg.startswith('ValueError') assert msg.endswith("world") + def test_excinfo_repr(): excinfo = py.test.raises(ValueError, h) s = repr(excinfo) assert s == "" + def test_excinfo_str(): excinfo = py.test.raises(ValueError, h) s = str(excinfo) @@ -266,20 +293,20 @@ def test_excinfo_str(): assert s.endswith("ValueError") assert len(s.split(":")) >= 3 # on windows it's 4 + def test_excinfo_errisinstance(): excinfo = py.test.raises(ValueError, h) assert excinfo.errisinstance(ValueError) + def test_excinfo_no_sourcecode(): try: exec ("raise ValueError()") except ValueError: excinfo = py.code.ExceptionInfo() s = str(excinfo.traceback[-1]) - if py.std.sys.version_info < (2,5): - assert s == " File '':1 in ?\n ???\n" - else: - assert s == " File '':1 in \n ???\n" + assert s == " File '':1 in \n ???\n" + def test_excinfo_no_python_sourcecode(tmpdir): #XXX: simplified locally testable version @@ -292,7 +319,7 @@ def test_excinfo_no_python_sourcecode(tmpdir): excinfo = py.test.raises(ValueError, template.render, h=h) for item in excinfo.traceback: - print(item) #XXX: for some reason jinja.Template.render is printed in full + print(item) # XXX: for some reason jinja.Template.render is printed in full item.source # shouldnt fail if item.path.basename == 'test.txt': assert str(item.source) == '{{ h()}}:' @@ -309,6 +336,7 @@ def test_entrysource_Queue_example(): s = str(source).strip() assert s.startswith("def get") + def test_codepath_Queue_example(): try: queue.Queue().get(timeout=0.001) @@ -320,6 +348,7 @@ def test_codepath_Queue_example(): assert path.basename.lower() == "queue.py" assert path.check() + class TestFormattedExcinfo: def pytest_funcarg__importasmod(self, request): def importasmod(source): @@ -355,6 +384,7 @@ def f(x): assert lines[0] == "| def f(x):" assert lines[1] == " pass" + @broken_on_modern_pytest def test_repr_source_excinfo(self): """ check if indentation is right """ pr = FormattedExcinfo() @@ -372,7 +402,6 @@ def f(): 'E assert 0' ] - def test_repr_source_not_existing(self): pr = FormattedExcinfo() co = compile("raise ValueError()", "", "exec") @@ -653,10 +682,11 @@ def entry(): p = FormattedExcinfo() def raiseos(): raise OSError(2) - monkeypatch.setattr(py.std.os, 'getcwd', raiseos) + monkeypatch.setattr('os.getcwd', raiseos) assert p._makepath(__file__) == __file__ reprtb = p.repr_traceback(excinfo) + @broken_on_modern_pytest def test_repr_excinfo_addouterr(self, importasmod): mod = importasmod(""" def entry(): @@ -699,6 +729,7 @@ def entry(): assert reprtb.extraline == "!!! Recursion detected (same locals & position)" assert str(reprtb) + @broken_on_modern_pytest def test_tb_entry_AssertionError(self, importasmod): # probably this test is a bit redundant # as py/magic/testing/test_assertion.py @@ -742,6 +773,7 @@ def toterminal(self, tw): x = py.builtin._totext(MyRepr()) assert x == py.builtin._totext("я", "utf-8") + @broken_on_modern_pytest def test_toterminal_long(self, importasmod): mod = importasmod(""" def g(x): @@ -768,6 +800,7 @@ def f(): assert tw.lines[9] == "" assert tw.lines[10].endswith("mod.py:3: ValueError") + @broken_on_modern_pytest def test_toterminal_long_missing_source(self, importasmod, tmpdir): mod = importasmod(""" def g(x): @@ -793,6 +826,7 @@ def f(): assert tw.lines[7] == "" assert tw.lines[8].endswith("mod.py:3: ValueError") + @broken_on_modern_pytest def test_toterminal_long_incomplete_source(self, importasmod, tmpdir): mod = importasmod(""" def g(x): @@ -818,6 +852,7 @@ def f(): assert tw.lines[7] == "" assert tw.lines[8].endswith("mod.py:3: ValueError") + @broken_on_modern_pytest def test_toterminal_long_filenames(self, importasmod): mod = importasmod(""" def f(): @@ -842,14 +877,16 @@ def f(): finally: old.chdir() - @py.test.mark.multi(reproptions=[ - {'style': style, 'showlocals': showlocals, - 'funcargs': funcargs, 'tbfilter': tbfilter - } for style in ("long", "short", "no") - for showlocals in (True, False) - for tbfilter in (True, False) - for funcargs in (True, False)]) - def test_format_excinfo(self, importasmod, reproptions): + @pytest.mark.parametrize('style', ("long", "short", "no")) + @pytest.mark.parametrize('showlocals', (True, False), + ids=['locals', 'nolocals']) + @pytest.mark.parametrize('tbfilter', (True, False), + ids=['tbfilter', 'nofilter']) + @pytest.mark.parametrize('funcargs', (True, False), + ids=['funcargs', 'nofuncargs']) + def test_format_excinfo(self, importasmod, + style, showlocals, tbfilter, funcargs): + mod = importasmod(""" def g(x): raise ValueError(x) @@ -858,11 +895,16 @@ def f(): """) excinfo = py.test.raises(ValueError, mod.f) tw = py.io.TerminalWriter(stringio=True) - repr = excinfo.getrepr(**reproptions) + repr = excinfo.getrepr( + style=style, + showlocals=showlocals, + funcargs=funcargs, + tbfilter=tbfilter + ) repr.toterminal(tw) assert tw.stringio.getvalue() - + @broken_on_modern_pytest def test_native_style(self): excinfo = self.excinfo_from_exec(""" assert 0 @@ -873,10 +915,9 @@ def test_native_style(self): assert s.startswith('Traceback (most recent call last):\n File') assert s.endswith('\nAssertionError: assert 0') assert 'exec (source.compile())' in s - # python 2.4 fails to get the source line for the assert - if py.std.sys.version_info >= (2, 5): - assert s.count('assert 0') == 2 + assert s.count('assert 0') == 2 + @broken_on_modern_pytest def test_traceback_repr_style(self, importasmod): mod = importasmod(""" def f(): diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 830de2c9..ca9a4227 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -1,6 +1,7 @@ from py.code import Source import py import sys +import inspect from py._code.source import _ast if _ast is not None: @@ -102,7 +103,7 @@ def test_source_strip_multiline(): def test_syntaxerror_rerepresentation(): ex = py.test.raises(SyntaxError, py.code.compile, 'xyz xyz') assert ex.value.lineno == 1 - assert ex.value.offset in (4,7) # XXX pypy/jython versus cpython? + assert ex.value.offset in (5, 7) # pypy/cpython difference assert ex.value.text.strip(), 'x x' def test_isparseable(): @@ -168,9 +169,9 @@ def f(): def f(): raise ValueError() """) - source1 = py.std.inspect.getsource(co1) + source1 = inspect.getsource(co1) assert 'KeyError' in source1 - source2 = py.std.inspect.getsource(co2) + source2 = inspect.getsource(co2) assert 'ValueError' in source2 def test_getstatement(self): @@ -251,7 +252,6 @@ def test_some(): assert getstatement(2, source).lines == source.lines[2:3] assert getstatement(3, source).lines == source.lines[3:4] - @py.test.mark.skipif("sys.version_info < (2,6)") def test_getstatementrange_out_of_bounds_py3(self): source = Source("if xxx:\n from .collections import something") r = source.getstatementrange(1) @@ -261,7 +261,6 @@ def test_getstatementrange_with_syntaxerror_issue7(self): source = Source(":") py.test.raises(SyntaxError, lambda: source.getstatementrange(0)) - @py.test.mark.skipif("sys.version_info < (2,6)") def test_compile_to_ast(self): import ast source = Source("x = 4") @@ -379,8 +378,6 @@ def g(): lines = deindent(source.splitlines()) assert lines == ['', 'def f():', ' def g():', ' pass', ' '] -@py.test.mark.xfail("sys.version_info[:3] < (2,7,0) or " - "((3,0) <= sys.version_info[:2] < (3,2))") def test_source_of_class_at_eof_without_newline(tmpdir): # this test fails because the implicit inspect.getsource(A) below # does not return the "x = 1" last line. @@ -451,7 +448,7 @@ class A(object): fspath, lineno = getfslineno(A) - _, A_lineno = py.std.inspect.findsource(A) + _, A_lineno = inspect.findsource(A) assert fspath.basename == "test_source.py" assert lineno == A_lineno @@ -459,7 +456,9 @@ class A(object): class B: pass B.__name__ = "B2" - assert getfslineno(B)[1] == -1 + # TODO: On CPython 3.9 this actually returns the line, + # should it? + # assert getfslineno(B)[1] == -1 def test_code_of_object_instance_with_call(): class A: @@ -513,11 +512,17 @@ def test_comments(): comment 4 """ ''' - for line in range(2,6): - assert str(getstatement(line, source)) == ' x = 1' - for line in range(6,10): - assert str(getstatement(line, source)) == ' assert False' - assert str(getstatement(10, source)) == '"""' + for line in range(2, 6): + assert str(getstatement(line, source)) == " x = 1" + if sys.version_info >= (3, 8) or hasattr(sys, "pypy_version_info"): + tqs_start = 8 + else: + tqs_start = 10 + assert str(getstatement(10, source)) == '"""' + for line in range(6, tqs_start): + assert str(getstatement(line, source)) == " assert False" + for line in range(tqs_start, 10): + assert str(getstatement(line, source)) == '"""\ncomment 4\n"""' def test_comment_in_statement(): source = '''test(foo=1, diff --git a/testing/io_/test_capture.py b/testing/io_/test_capture.py index 5745e12a..b5fedd0a 100644 --- a/testing/io_/test_capture.py +++ b/testing/io_/test_capture.py @@ -366,7 +366,7 @@ def test_intermingling(self): def test_callcapture(self): def func(x, y): print (x) - py.std.sys.stderr.write(str(y)) + sys.stderr.write(str(y)) return 42 res, out, err = py.io.StdCaptureFD.call(func, 3, y=4) @@ -460,7 +460,7 @@ def func(x, y): assert err.startswith("4") @needsdup -@py.test.mark.multi(use=[True, False]) +@py.test.mark.parametrize('use', [True, False]) def test_fdcapture_tmpfile_remains_the_same(tmpfile, use): if not use: tmpfile = True @@ -472,7 +472,7 @@ def test_fdcapture_tmpfile_remains_the_same(tmpfile, use): capfile2 = cap.err.tmpfile assert capfile2 == capfile -@py.test.mark.multi(method=['StdCapture', 'StdCaptureFD']) +@py.test.mark.parametrize('method', ['StdCapture', 'StdCaptureFD']) def test_capturing_and_logging_fundamentals(testdir, method): if method == "StdCaptureFD" and not hasattr(os, 'dup'): py.test.skip("need os.dup") diff --git a/testing/io_/test_saferepr.py b/testing/io_/test_saferepr.py index 1ed9c4fa..97be1416 100644 --- a/testing/io_/test_saferepr.py +++ b/testing/io_/test_saferepr.py @@ -39,10 +39,7 @@ class BrokenReprException(Exception): assert 'Exception' in saferepr(BrokenRepr(Exception("broken"))) s = saferepr(BrokenReprException("really broken")) assert 'TypeError' in s - if py.std.sys.version_info < (2,6): - assert 'unknown' in saferepr(BrokenRepr("string")) - else: - assert 'TypeError' in saferepr(BrokenRepr("string")) + assert 'TypeError' in saferepr(BrokenRepr("string")) s2 = saferepr(BrokenRepr(BrokenReprException('omg even worse'))) assert 'NameError' not in s2 diff --git a/testing/io_/test_terminalwriter.py b/testing/io_/test_terminalwriter.py index 0a15541b..1eef7f7d 100644 --- a/testing/io_/test_terminalwriter.py +++ b/testing/io_/test_terminalwriter.py @@ -1,3 +1,4 @@ +from collections import namedtuple import py import os, sys @@ -10,16 +11,22 @@ def test_get_terminal_width(): assert x == terminalwriter.get_terminal_width def test_getdimensions(monkeypatch): - fcntl = py.test.importorskip("fcntl") - import struct - l = [] - monkeypatch.setattr(fcntl, 'ioctl', lambda *args: l.append(args)) - try: - terminalwriter._getdimensions() - except (TypeError, struct.error): - pass - assert len(l) == 1 - assert l[0][0] == 1 + if sys.version_info >= (3, 3): + import shutil + Size = namedtuple('Size', 'lines columns') + monkeypatch.setattr(shutil, 'get_terminal_size', lambda: Size(60, 100)) + assert terminalwriter._getdimensions() == (60, 100) + else: + fcntl = py.test.importorskip("fcntl") + import struct + l = [] + monkeypatch.setattr(fcntl, 'ioctl', lambda *args: l.append(args)) + try: + terminalwriter._getdimensions() + except (TypeError, struct.error): + pass + assert len(l) == 1 + assert l[0][0] == 1 def test_terminal_width_COLUMNS(monkeypatch): """ Dummy test for get_terminal_width @@ -73,7 +80,7 @@ def isatty(self): monkeypatch.undo() def test_terminalwriter_file_unicode(tmpdir): - f = py.std.codecs.open(str(tmpdir.join("xyz")), "wb", "utf8") + f = codecs.open(str(tmpdir.join("xyz")), "wb", "utf8") tw = py.io.TerminalWriter(file=f) assert tw.encoding == "utf8" @@ -89,7 +96,7 @@ def test_unicode_encoding(): def test_unicode_on_file_with_ascii_encoding(tmpdir, monkeypatch, encoding): msg = py.builtin._totext('hell\xf6', "latin1") #pytest.raises(UnicodeEncodeError, lambda: bytes(msg)) - f = py.std.codecs.open(str(tmpdir.join("x")), "w", encoding) + f = codecs.open(str(tmpdir.join("x")), "w", encoding) tw = py.io.TerminalWriter(f) tw.line(msg) f.close() @@ -158,6 +165,12 @@ def test_sep_with_title(self, tw): assert len(l) == 1 assert l[0] == "-" * 26 + " hello " + "-" * (27-win32) + "\n" + def test_sep_longer_than_width(self, tw): + tw.sep('-', 'a' * 10, fullwidth=5) + line, = tw.getlines() + # even though the string is wider than the line, still have a separator + assert line == '- aaaaaaaaaa -\n' + @py.test.mark.skipif("sys.platform == 'win32'") def test__escaped(self, tw): text2 = tw._escaped("hello", (31)) @@ -226,6 +239,27 @@ class fil: assert l == set(["2"]) +def test_chars_on_current_line(): + tw = py.io.TerminalWriter(stringio=True) + + written = [] + + def write_and_check(s, expected): + tw.write(s, bold=True) + written.append(s) + assert tw.chars_on_current_line == expected + assert tw.stringio.getvalue() == ''.join(written) + + write_and_check('foo', 3) + write_and_check('bar', 6) + write_and_check('\n', 0) + write_and_check('\n', 0) + write_and_check('\n\n\n', 0) + write_and_check('\nfoo', 3) + write_and_check('\nfbar\nhello', 5) + write_and_check('10', 7) + + @pytest.mark.skipif(sys.platform == "win32", reason="win32 has no native ansi") def test_attr_hasmarkup(): tw = py.io.TerminalWriter(stringio=True) diff --git a/testing/io_/test_terminalwriter_linewidth.py b/testing/io_/test_terminalwriter_linewidth.py new file mode 100644 index 00000000..e6d84fbf --- /dev/null +++ b/testing/io_/test_terminalwriter_linewidth.py @@ -0,0 +1,56 @@ +# coding: utf-8 +from __future__ import unicode_literals + +from py._io.terminalwriter import TerminalWriter + + +def test_terminal_writer_line_width_init(): + tw = TerminalWriter() + assert tw.chars_on_current_line == 0 + assert tw.width_of_current_line == 0 + + +def test_terminal_writer_line_width_update(): + tw = TerminalWriter() + tw.write('hello world') + assert tw.chars_on_current_line == 11 + assert tw.width_of_current_line == 11 + + +def test_terminal_writer_line_width_update_with_newline(): + tw = TerminalWriter() + tw.write('hello\nworld') + assert tw.chars_on_current_line == 5 + assert tw.width_of_current_line == 5 + + +def test_terminal_writer_line_width_update_with_wide_text(): + tw = TerminalWriter() + tw.write('乇乂ㄒ尺卂 ㄒ卄丨匚匚') + assert tw.chars_on_current_line == 11 + assert tw.width_of_current_line == 21 # 5*2 + 1 + 5*2 + + +def test_terminal_writer_line_width_update_with_wide_bytes(): + tw = TerminalWriter() + tw.write('乇乂ㄒ尺卂 ㄒ卄丨匚匚'.encode('utf-8')) + assert tw.chars_on_current_line == 11 + assert tw.width_of_current_line == 21 + + +def test_terminal_writer_line_width_composed(): + tw = TerminalWriter() + text = 'café food' + assert len(text) == 9 + tw.write(text) + assert tw.chars_on_current_line == 9 + assert tw.width_of_current_line == 9 + + +def test_terminal_writer_line_width_combining(): + tw = TerminalWriter() + text = 'café food' + assert len(text) == 10 + tw.write(text) + assert tw.chars_on_current_line == 10 + assert tw.width_of_current_line == 9 diff --git a/testing/log/test_log.py b/testing/log/test_log.py index b41bc3a5..5c706d9b 100644 --- a/testing/log/test_log.py +++ b/testing/log/test_log.py @@ -1,10 +1,10 @@ import py -import sys from py._log.log import default_keywordmapper callcapture = py.io.StdCapture.call + def setup_module(mod): mod._oldstate = default_keywordmapper.getstate() @@ -13,6 +13,7 @@ def teardown_module(mod): class TestLogProducer: def setup_method(self, meth): + from py._log.log import default_keywordmapper default_keywordmapper.setstate(_oldstate) def test_getstate_setstate(self): diff --git a/testing/log/test_warning.py b/testing/log/test_warning.py index 8c89cf8a..a460c319 100644 --- a/testing/log/test_warning.py +++ b/testing/log/test_warning.py @@ -1,8 +1,18 @@ +import sys +from distutils.version import LooseVersion + import pytest + import py mypath = py.path.local(__file__).new(ext=".py") + +win = sys.platform.startswith('win') +pytestmark = pytest.mark.skipif(win and LooseVersion(pytest.__version__) >= LooseVersion('3.1'), + reason='apiwarn is not compatible with pytest >= 3.1 (#162)') + + @pytest.mark.xfail def test_forwarding_to_warnings_module(): pytest.deprecated_call(py.log._apiwarn, "1.3", "..") diff --git a/testing/path/common.py b/testing/path/common.py index 274ced46..d69a1c39 100644 --- a/testing/path/common.py +++ b/testing/path/common.py @@ -477,10 +477,7 @@ def setuptestfs(path): otherdir.ensure('__init__.py') module_a = otherdir.ensure('a.py') - if sys.version_info >= (2,6): - module_a.write('from .b import stuff as result\n') - else: - module_a.write('from b import stuff as result\n') + module_a.write('from .b import stuff as result\n') module_b = otherdir.ensure('b.py') module_b.write('stuff="got it"\n') module_c = otherdir.ensure('c.py') diff --git a/testing/path/conftest.py b/testing/path/conftest.py index a9711b2c..84fb5c82 100644 --- a/testing/path/conftest.py +++ b/testing/path/conftest.py @@ -47,7 +47,7 @@ def getrepowc(tmpdir, reponame='basetestrepo', wcname='wc'): print_("created svn repository", repo) wcdir.ensure(dir=1) wc = py.path.svnwc(wcdir) - if py.std.sys.platform == 'win32': + if sys.platform == 'win32': repourl = "file://" + '/' + str(repo).replace('\\', '/') else: repourl = "file://%s" % repo diff --git a/testing/path/test_cacheutil.py b/testing/path/test_cacheutil.py index 0b5cd313..c9fc0746 100644 --- a/testing/path/test_cacheutil.py +++ b/testing/path/test_cacheutil.py @@ -1,6 +1,8 @@ -import py +import pytest from py._path import cacheutil +import time + class BasicCacheAPITest: cache = None def test_getorbuild(self): @@ -10,21 +12,22 @@ def test_getorbuild(self): assert val == 42 def test_cache_get_key_error(self): - py.test.raises(KeyError, "self.cache._getentry(-23)") + pytest.raises(KeyError, "self.cache._getentry(-23)") def test_delentry_non_raising(self): - val = self.cache.getorbuild(100, lambda: 100) + self.cache.getorbuild(100, lambda: 100) self.cache.delentry(100) - py.test.raises(KeyError, "self.cache._getentry(100)") + pytest.raises(KeyError, "self.cache._getentry(100)") def test_delentry_raising(self): - val = self.cache.getorbuild(100, lambda: 100) + self.cache.getorbuild(100, lambda: 100) self.cache.delentry(100) - py.test.raises(KeyError, "self.cache.delentry(100, raising=True)") + pytest.raises(KeyError, self.cache.delentry, 100, raising=True) def test_clear(self): self.cache.clear() + class TestBuildcostAccess(BasicCacheAPITest): cache = cacheutil.BuildcostAccessCache(maxentries=128) @@ -36,6 +39,7 @@ def test_cache_works_somewhat_simple(self, monkeypatch): # test fail randomly. Let's rather use incrementing # numbers instead. l = [0] + def counter(): l[0] = l[0] + 1 return l[0] @@ -65,20 +69,21 @@ class TestAging(BasicCacheAPITest): def test_cache_eviction(self): self.cache.getorbuild(17, lambda: 17) - endtime = py.std.time.time() + self.maxsecs * 10 - while py.std.time.time() < endtime: + endtime = time.time() + self.maxsecs * 10 + while time.time() < endtime: try: self.cache._getentry(17) except KeyError: break - py.std.time.sleep(self.maxsecs*0.3) + time.sleep(self.maxsecs*0.3) else: - py.test.fail("waiting for cache eviction failed") + pytest.fail("waiting for cache eviction failed") + def test_prune_lowestweight(): maxsecs = 0.05 cache = cacheutil.AgingCache(maxentries=10, maxseconds=maxsecs) for x in range(cache.maxentries): cache.getorbuild(x, lambda: x) - py.std.time.sleep(maxsecs*1.1) + time.sleep(maxsecs*1.1) cache.getorbuild(cache.maxentries+1, lambda: 42) diff --git a/testing/path/test_local.py b/testing/path/test_local.py index deb4fcad..1b9a7923 100644 --- a/testing/path/test_local.py +++ b/testing/path/test_local.py @@ -4,13 +4,16 @@ import time import py import pytest -import os, sys +import os +import sys +import multiprocessing from py.path import local import common failsonjython = py.test.mark.xfail("sys.platform.startswith('java')") -failsonjywin32 = py.test.mark.xfail("sys.platform.startswith('java') " - "and getattr(os, '_name', None) == 'nt'") +failsonjywin32 = py.test.mark.xfail( + "sys.platform.startswith('java') " + "and getattr(os, '_name', None) == 'nt'") win32only = py.test.mark.skipif( "not (sys.platform == 'win32' or getattr(os, '_name', None) == 'nt')") skiponwin32 = py.test.mark.skipif( @@ -19,24 +22,41 @@ ATIME_RESOLUTION = 0.01 -def pytest_funcarg__path1(request): - def setup(): - path1 = request.getfuncargvalue("tmpdir") - common.setuptestfs(path1) - return path1 - def teardown(path1): - # post check - assert path1.join("samplefile").check() - return request.cached_setup(setup, teardown, scope="session") +@pytest.yield_fixture(scope="session") +def path1(tmpdir_factory): + path = tmpdir_factory.mktemp('path') + common.setuptestfs(path) + yield path + assert path.join("samplefile").check() -def pytest_funcarg__fake_fspath_obj(request): + +@pytest.fixture +def fake_fspath_obj(request): class FakeFSPathClass(object): def __init__(self, path): self._path = path + def __fspath__(self): return self._path + return FakeFSPathClass(os.path.join("this", "is", "a", "fake", "path")) + +def batch_make_numbered_dirs(rootdir, repeats): + try: + for i in range(repeats): + dir_ = py.path.local.make_numbered_dir(prefix='repro-', rootdir=rootdir) + file_ = dir_.join('foo') + file_.write('%s' % i) + actual = int(file_.read()) + assert actual == i, 'int(file_.read()) is %s instead of %s' % (actual, i) + dir_.join('.lock').remove(ignore_errors=True) + return True + except KeyboardInterrupt: + # makes sure that interrupting test session won't hang it + os.exit(2) + + class TestLocalPath(common.CommonFSTests): def test_join_normpath(self, tmpdir): assert tmpdir.join(".") == tmpdir @@ -49,7 +69,7 @@ def test_join_normpath(self, tmpdir): def test_dirpath_abs_no_abs(self, tmpdir): p = tmpdir.join('foo') assert p.dirpath('/bar') == tmpdir.join('bar') - assert tmpdir.dirpath('/bar', abs=True) == py.path.local('/bar') + assert tmpdir.dirpath('/bar', abs=True) == local('/bar') def test_gethash(self, tmpdir): md5 = py.builtin._tryimport('md5', 'hashlib').md5 @@ -83,7 +103,8 @@ def test_remove_removes_dir_and_readonly_file(self, tmpdir): def test_remove_routes_ignore_errors(self, tmpdir, monkeypatch): l = [] - monkeypatch.setattr(py.std.shutil, 'rmtree', + monkeypatch.setattr( + 'shutil.rmtree', lambda *args, **kwargs: l.append(kwargs)) tmpdir.remove() assert not l[0]['ignore_errors'] @@ -93,7 +114,7 @@ def test_remove_routes_ignore_errors(self, tmpdir, monkeypatch): assert l[0]['ignore_errors'] == val def test_initialize_curdir(self): - assert str(local()) == py.std.os.getcwd() + assert str(local()) == os.getcwd() @skiponwin32 def test_chdir_gone(self, path1): @@ -104,6 +125,19 @@ def test_chdir_gone(self, path1): assert path1.chdir() is None assert os.getcwd() == str(path1) + with pytest.raises(py.error.ENOENT): + with p.as_cwd(): + raise NotImplementedError + + @skiponwin32 + def test_chdir_gone_in_as_cwd(self, path1): + p = path1.ensure("dir_to_be_removed", dir=1) + p.chdir() + p.remove() + + with path1.as_cwd() as old: + assert old is None + def test_as_cwd(self, path1): dir = path1.ensure("subdir", dir=1) old = py.path.local() @@ -125,12 +159,22 @@ def test_initialize_reldir(self, path1): p = local('samplefile') assert p.check() - @pytest.mark.xfail("sys.version_info < (2,6) and sys.platform == 'win32'") def test_tilde_expansion(self, monkeypatch, tmpdir): monkeypatch.setenv("HOME", str(tmpdir)) p = py.path.local("~", expanduser=True) assert p == os.path.expanduser("~") + @pytest.mark.skipif( + not sys.platform.startswith("win32"), reason="case insensitive only on windows" + ) + def test_eq_hash_are_case_insensitive_on_windows(self): + a = py.path.local("/some/path") + b = py.path.local("/some/PATH") + assert a == b + assert hash(a) == hash(b) + assert a in {b} + assert a in {b: 'b'} + def test_eq_with_strings(self, path1): path1 = path1.join('sampledir') path2 = str(path1) @@ -141,7 +185,31 @@ def test_eq_with_strings(self, path1): assert path2 != path3 def test_eq_with_none(self, path1): - assert path1 != None + assert path1 != None # noqa: E711 + + @pytest.mark.skipif( + sys.platform.startswith("win32"), reason="cannot remove cwd on Windows" + ) + @pytest.mark.skipif( + sys.version_info < (3, 0) or sys.version_info >= (3, 5), + reason="only with Python 3 before 3.5" + ) + def test_eq_with_none_and_custom_fspath(self, monkeypatch, path1): + import os + import shutil + import tempfile + + d = tempfile.mkdtemp() + monkeypatch.chdir(d) + shutil.rmtree(d) + + monkeypatch.delitem(sys.modules, 'pathlib', raising=False) + monkeypatch.setattr(sys, 'path', [''] + sys.path) + + with pytest.raises(FileNotFoundError): + import pathlib # noqa: F401 + + assert path1 != None # noqa: E711 def test_eq_non_ascii_unicode(self, path1): path2 = path1.join(u'temp') @@ -160,7 +228,7 @@ def test_gt_with_strings(self, path1): assert path2 < "ttt" assert "ttt" > path2 path4 = path1.join("aaa") - l = [path2, path4,path3] + l = [path2, path4, path3] assert sorted(l) == [path4, path2, path3] def test_open_and_ensure(self, path1): @@ -174,14 +242,15 @@ def test_write_and_ensure(self, path1): p.write("hello", ensure=1) assert p.read() == "hello" - @py.test.mark.multi(bin=(False, True)) + @py.test.mark.parametrize('bin', (False, True)) def test_dump(self, tmpdir, bin): path = tmpdir.join("dumpfile%s" % int(bin)) try: - d = {'answer' : 42} + d = {'answer': 42} path.dump(d, bin=bin) f = path.open('rb+') - dnew = py.std.pickle.load(f) + import pickle + dnew = pickle.load(f) assert d == dnew finally: f.close() @@ -192,7 +261,7 @@ def test_setmtime(self): import time try: fd, name = tempfile.mkstemp() - py.std.os.close(fd) + os.close(fd) except AttributeError: name = tempfile.mktemp() open(name, 'w').close() @@ -205,7 +274,7 @@ def test_setmtime(self): path.setmtime() assert path.mtime() != mtime finally: - py.std.os.remove(name) + os.remove(name) def test_normpath(self, path1): new1 = path1.join("/otherdir") @@ -233,12 +302,12 @@ def test_chdir(self, tmpdir): try: res = tmpdir.chdir() assert str(res) == str(old) - assert py.std.os.getcwd() == str(tmpdir) + assert os.getcwd() == str(tmpdir) finally: old.chdir() def test_ensure_filepath_withdir(self, tmpdir): - newfile = tmpdir.join('test1','test') + newfile = tmpdir.join('test1', 'test') newfile.ensure() assert newfile.check(file=1) newfile.write("42") @@ -253,7 +322,7 @@ def test_ensure_filepath_withoutdir(self, tmpdir): assert newfile.check(file=1) def test_ensure_dirpath(self, tmpdir): - newfile = tmpdir.join('test1','testfile') + newfile = tmpdir.join('test1', 'testfile') t = newfile.ensure(dir=1) assert t == newfile assert newfile.check(dir=1) @@ -293,8 +362,8 @@ def test_long_filenames(self, tmpdir): assert l2.read() == 'foo' def test_visit_depth_first(self, tmpdir): - p1 = tmpdir.ensure("a","1") - p2 = tmpdir.ensure("b","2") + tmpdir.ensure("a", "1") + tmpdir.ensure("b", "2") p3 = tmpdir.ensure("breadth") l = list(tmpdir.visit(lambda x: x.check(file=1))) assert len(l) == 3 @@ -302,8 +371,8 @@ def test_visit_depth_first(self, tmpdir): assert l[2] == p3 def test_visit_rec_fnmatch(self, tmpdir): - p1 = tmpdir.ensure("a","123") - p2 = tmpdir.ensure(".b","345") + p1 = tmpdir.ensure("a", "123") + tmpdir.ensure(".b", "345") l = list(tmpdir.visit("???", rec="[!.]*")) assert len(l) == 1 # check that breadth comes last @@ -332,6 +401,13 @@ def test_fspath_protocol_other_class(self, fake_fspath_obj): assert py_path.join(fake_fspath_obj).strpath == os.path.join( py_path.strpath, str_path) + def test_make_numbered_dir_multiprocess_safe(self, tmpdir): + # https://github.com/pytest-dev/py/issues/30 + pool = multiprocessing.Pool() + results = [pool.apply_async(batch_make_numbered_dirs, [tmpdir, 100]) for _ in range(20)] + for r in results: + assert r.get() + class TestExecutionOnWindows: pytestmark = win32only @@ -361,17 +437,16 @@ def test_sysfind_absolute(self): assert y == x def test_sysfind_multiple(self, tmpdir, monkeypatch): - monkeypatch.setenv('PATH', - "%s:%s" % (tmpdir.ensure('a'), - tmpdir.join('b')), - prepend=":") + monkeypatch.setenv('PATH', "%s:%s" % ( + tmpdir.ensure('a'), + tmpdir.join('b')), + prepend=":") tmpdir.ensure('b', 'a') - checker = lambda x: x.dirpath().basename == 'b' - x = py.path.local.sysfind('a', checker=checker) + x = py.path.local.sysfind( + 'a', checker=lambda x: x.dirpath().basename == 'b') assert x.basename == 'a' assert x.dirpath().basename == 'b' - checker = lambda x: None - assert py.path.local.sysfind('a', checker=checker) is None + assert py.path.local.sysfind('a', checker=lambda x: None) is None def test_sysexec(self): x = py.path.local.sysfind('ls') @@ -381,9 +456,8 @@ def test_sysexec(self): def test_sysexec_failing(self): x = py.path.local.sysfind('false') - py.test.raises(py.process.cmdexec.Error, """ + with pytest.raises(py.process.cmdexec.Error): x.sysexec('aksjdkasjd') - """) def test_make_numbered_dir(self, tmpdir): tmpdir.ensure('base.not_an_int', dir=1) @@ -391,18 +465,36 @@ def test_make_numbered_dir(self, tmpdir): numdir = local.make_numbered_dir(prefix='base.', rootdir=tmpdir, keep=2, lock_timeout=0) assert numdir.check() - assert numdir.basename == 'base.%d' %i - if i>=1: + assert numdir.basename == 'base.%d' % i + if i >= 1: assert numdir.new(ext=str(i-1)).check() - if i>=2: + if i >= 2: assert numdir.new(ext=str(i-2)).check() - if i>=3: + if i >= 3: assert not numdir.new(ext=str(i-3)).check() + def test_make_numbered_dir_case(self, tmpdir): + """make_numbered_dir does not make assumptions on the underlying + filesystem based on the platform and will assume it _could_ be case + insensitive. + + See issues: + - https://github.com/pytest-dev/pytest/issues/708 + - https://github.com/pytest-dev/pytest/issues/3451 + """ + d1 = local.make_numbered_dir( + prefix='CAse.', rootdir=tmpdir, keep=2, lock_timeout=0, + ) + d2 = local.make_numbered_dir( + prefix='caSE.', rootdir=tmpdir, keep=2, lock_timeout=0, + ) + assert str(d1).lower() != str(d2).lower() + assert str(d2).endswith('.1') + def test_make_numbered_dir_NotImplemented_Error(self, tmpdir, monkeypatch): def notimpl(x, y): raise NotImplementedError(42) - monkeypatch.setattr(py.std.os, 'symlink', notimpl) + monkeypatch.setattr(os, 'symlink', notimpl) x = tmpdir.make_numbered_dir(rootdir=tmpdir, lock_timeout=0) assert x.relto(tmpdir) assert x.check() @@ -412,15 +504,15 @@ def test_locked_make_numbered_dir(self, tmpdir): numdir = local.make_numbered_dir(prefix='base2.', rootdir=tmpdir, keep=2) assert numdir.check() - assert numdir.basename == 'base2.%d' %i + assert numdir.basename == 'base2.%d' % i for j in range(i): assert numdir.new(ext=str(j)).check() def test_error_preservation(self, path1): - py.test.raises (EnvironmentError, path1.join('qwoeqiwe').mtime) - py.test.raises (EnvironmentError, path1.join('qwoeqiwe').read) + py.test.raises(EnvironmentError, path1.join('qwoeqiwe').mtime) + py.test.raises(EnvironmentError, path1.join('qwoeqiwe').read) - #def test_parentdirmatch(self): + # def test_parentdirmatch(self): # local.parentdirmatch('std', startmodule=__name__) # @@ -431,17 +523,26 @@ def test_pyimport(self, path1): assert obj.x == 42 assert obj.__name__ == 'execfile' - def test_pyimport_renamed_dir_creates_mismatch(self, tmpdir): + def test_pyimport_renamed_dir_creates_mismatch(self, tmpdir, monkeypatch): p = tmpdir.ensure("a", "test_x123.py") p.pyimport() tmpdir.join("a").move(tmpdir.join("b")) - pytest.raises(tmpdir.ImportMismatchError, - lambda: tmpdir.join("b", "test_x123.py").pyimport()) + with pytest.raises(tmpdir.ImportMismatchError): + tmpdir.join("b", "test_x123.py").pyimport() + + # Errors can be ignored. + monkeypatch.setenv('PY_IGNORE_IMPORTMISMATCH', '1') + tmpdir.join("b", "test_x123.py").pyimport() + + # PY_IGNORE_IMPORTMISMATCH=0 does not ignore error. + monkeypatch.setenv('PY_IGNORE_IMPORTMISMATCH', '0') + with pytest.raises(tmpdir.ImportMismatchError): + tmpdir.join("b", "test_x123.py").pyimport() def test_pyimport_messy_name(self, tmpdir): # http://bitbucket.org/hpk42/py-trunk/issue/129 path = tmpdir.ensure('foo__init__.py') - obj = path.pyimport() + path.pyimport() def test_pyimport_dir(self, tmpdir): p = tmpdir.join("hello_123") @@ -488,7 +589,7 @@ def test_pyimport_and_import(self, tmpdir): def test_pyimport_check_filepath_consistency(self, monkeypatch, tmpdir): name = 'pointsback123' - ModuleType = type(py.std.os) + ModuleType = type(os) p = tmpdir.ensure(name + '.py') for ending in ('.pyc', '$py.class', '.pyo'): mod = ModuleType(name) @@ -502,8 +603,7 @@ def test_pyimport_check_filepath_consistency(self, monkeypatch, tmpdir): pseudopath = tmpdir.ensure(name+"123.py") mod.__file__ = str(pseudopath) monkeypatch.setitem(sys.modules, name, mod) - excinfo = py.test.raises(pseudopath.ImportMismatchError, - "p.pyimport()") + excinfo = py.test.raises(pseudopath.ImportMismatchError, p.pyimport) modname, modfile, orig = excinfo.value.args assert modname == name assert modfile == pseudopath @@ -529,6 +629,39 @@ def test_ensuresyspath_append(self, tmpdir): assert str(root1) not in sys.path[:-1] +class TestImportlibImport: + pytestmark = py.test.mark.skipif("sys.version_info < (3, 5)") + + OPTS = {'ensuresyspath': 'importlib'} + + def test_pyimport(self, path1): + obj = path1.join('execfile.py').pyimport(**self.OPTS) + assert obj.x == 42 + assert obj.__name__ == 'execfile' + + def test_pyimport_dir_fails(self, tmpdir): + p = tmpdir.join("hello_123") + p.ensure("__init__.py") + with pytest.raises(ImportError): + p.pyimport(**self.OPTS) + + def test_pyimport_execfile_different_name(self, path1): + obj = path1.join('execfile.py').pyimport(modname="0x.y.z", **self.OPTS) + assert obj.x == 42 + assert obj.__name__ == '0x.y.z' + + def test_pyimport_relative_import_fails(self, path1): + otherdir = path1.join('otherdir') + with pytest.raises(ImportError): + otherdir.join('a.py').pyimport(**self.OPTS) + + def test_pyimport_doesnt_use_sys_modules(self, tmpdir): + p = tmpdir.ensure('file738jsk.py') + mod = p.pyimport(**self.OPTS) + assert mod.__name__ == 'file738jsk' + assert 'file738jsk' not in sys.modules + + def test_pypkgdir(tmpdir): pkg = tmpdir.ensure('pkg1', dir=1) pkg.ensure("__init__.py") @@ -536,14 +669,16 @@ def test_pypkgdir(tmpdir): assert pkg.pypkgpath() == pkg assert pkg.join('subdir', '__init__.py').pypkgpath() == pkg + def test_pypkgdir_unimportable(tmpdir): - pkg = tmpdir.ensure('pkg1-1', dir=1) # unimportable + pkg = tmpdir.ensure('pkg1-1', dir=1) # unimportable pkg.ensure("__init__.py") subdir = pkg.ensure("subdir/__init__.py").dirpath() assert subdir.pypkgpath() == subdir assert subdir.ensure("xyz.py").pypkgpath() == subdir assert not pkg.pypkgpath() + def test_isimportable(): from py._path.local import isimportable assert not isimportable("") @@ -555,17 +690,20 @@ def test_isimportable(): assert not isimportable("x-1") assert not isimportable("x:1") + def test_homedir_from_HOME(monkeypatch): path = os.getcwd() monkeypatch.setenv("HOME", path) assert py.path.local._gethomedir() == py.path.local(path) + def test_homedir_not_exists(monkeypatch): monkeypatch.delenv("HOME", raising=False) monkeypatch.delenv("HOMEDRIVE", raising=False) homedir = py.path.local._gethomedir() assert homedir is None + def test_samefile(tmpdir): assert tmpdir.samefile(tmpdir) p = tmpdir.ensure("hello") @@ -577,14 +715,29 @@ def test_samefile(tmpdir): p2 = p.__class__(str(p).upper()) assert p1.samefile(p2) +@pytest.mark.skipif(not hasattr(os, "symlink"), reason="os.symlink not available") +def test_samefile_symlink(tmpdir): + p1 = tmpdir.ensure("foo.txt") + p2 = tmpdir.join("linked.txt") + try: + os.symlink(str(p1), str(p2)) + except (OSError, NotImplementedError) as e: + # on Windows this might fail if the user doesn't have special symlink permissions + # pypy3 on Windows doesn't implement os.symlink and raises NotImplementedError + pytest.skip(str(e.args[0])) + + assert p1.samefile(p2) + def test_listdir_single_arg(tmpdir): tmpdir.ensure("hello") assert tmpdir.listdir("hello")[0].basename == "hello" + def test_mkdtemp_rootdir(tmpdir): dtmp = local.mkdtemp(rootdir=tmpdir) assert tmpdir.listdir() == [dtmp] + class TestWINLocalPath: pytestmark = win32only @@ -626,7 +779,7 @@ def test_allow_unix_style_paths(self, path1): def test_sysfind_in_currentdir(self, path1): cmd = py.path.local.sysfind('cmd') - root = cmd.new(dirname='', basename='') # c:\ in most installations + root = cmd.new(dirname='', basename='') # c:\ in most installations with root.as_cwd(): x = py.path.local.sysfind(cmd.relto(root)) assert x.check(file=1) @@ -640,6 +793,7 @@ def test_fnmatch_file_abspath_posix_pattern_on_win32(self, tmpdir): pattern = posixpath.sep.join([str(tmpdir), "*", "b"]) assert b.fnmatch(pattern) + class TestPOSIXLocalPath: pytestmark = skiponwin32 @@ -698,7 +852,7 @@ def test_symlink_isdir(self, tmpdir): def test_symlink_remove(self, tmpdir): linkpath = tmpdir.join('test') - linkpath.mksymlinkto(linkpath) # point to itself + linkpath.mksymlinkto(linkpath) # point to itself assert linkpath.check(link=1) linkpath.remove() assert not linkpath.check() @@ -774,7 +928,7 @@ def test_commondir_nocommon(self, path1): def test_join_to_root(self, path1): root = path1.parts()[0] assert len(str(root)) == 1 - assert str(root.join('a')) == '//a' # posix allows two slashes + assert str(root.join('a')) == '/a' def test_join_root_to_root_with_no_abs(self, path1): nroot = path1.join('/') @@ -792,7 +946,7 @@ def test_chmod_simple_int(self, path1): def test_chmod_rec_int(self, path1): # XXX fragile test - recfilter = lambda x: x.check(dotfile=0, link=0) + def recfilter(x): return x.check(dotfile=0, link=0) oldmodes = {} for x in path1.visit(rec=recfilter): oldmodes[x] = x.stat().mode @@ -801,7 +955,7 @@ def test_chmod_rec_int(self, path1): for x in path1.visit(rec=recfilter): assert x.stat().mode & int("777", 8) == int("772", 8) finally: - for x,y in oldmodes.items(): + for x, y in oldmodes.items(): x.chmod(y) def test_copy_archiving(self, tmpdir): @@ -869,7 +1023,7 @@ def test_chown_identity_rec_mayfail(self, path1): class TestUnicodePy2Py3: def test_join_ensure(self, tmpdir, monkeypatch): - if sys.version_info >= (3,0) and "LANG" not in os.environ: + if sys.version_info >= (3, 0) and "LANG" not in os.environ: pytest.skip("cannot run test without locale") x = py.path.local(tmpdir.strpath) part = "hällo" @@ -877,14 +1031,15 @@ def test_join_ensure(self, tmpdir, monkeypatch): assert x.join(part) == y def test_listdir(self, tmpdir): - if sys.version_info >= (3,0) and "LANG" not in os.environ: + if sys.version_info >= (3, 0) and "LANG" not in os.environ: pytest.skip("cannot run test without locale") x = py.path.local(tmpdir.strpath) part = "hällo" y = x.ensure(part) assert x.listdir(part)[0] == y - @pytest.mark.xfail(reason="changing read/write might break existing usages") + @pytest.mark.xfail( + reason="changing read/write might break existing usages") def test_read_write(self, tmpdir): x = tmpdir.join("hello") part = py.builtin._totext("hällo", "utf8") @@ -893,6 +1048,7 @@ def test_read_write(self, tmpdir): x.write(part.encode(sys.getdefaultencoding())) assert x.read() == part.encode(sys.getdefaultencoding()) + class TestBinaryAndTextMethods: def test_read_binwrite(self, tmpdir): x = tmpdir.join("hello") diff --git a/testing/path/test_svnauth.py b/testing/path/test_svnauth.py index b3f36656..654f0332 100644 --- a/testing/path/test_svnauth.py +++ b/testing/path/test_svnauth.py @@ -1,11 +1,11 @@ import py -import svntestbase from py.path import SvnAuth import time import sys svnbin = py.path.local.sysfind('svn') + def make_repo_auth(repo, userdata): """ write config to repo @@ -277,7 +277,7 @@ def __init__(self, request): if sys.platform == 'win32': repodir = '/' + str(repodir).replace('\\', '/') self.repo = py.path.svnurl("file://%s" % repodir) - if py.std.sys.platform == 'win32': + if sys.platform == 'win32': # remove trailing slash... repodir = repodir[1:] self.repopath = py.path.local(repodir) @@ -322,6 +322,12 @@ def test_log(self, setup): assert log[0].msg == 'added foo.txt' def test_switch(self, setup): + import pytest + try: + import xdist + pytest.skip('#160: fails under xdist') + except ImportError: + pass wc = py.path.svnwc(setup.temppath, auth=setup.auth) svnurl = 'svn://localhost:%s/%s' % (setup.port, setup.repopath.basename) wc.checkout(svnurl) diff --git a/testing/path/test_svnwc.py b/testing/path/test_svnwc.py index 9e6b524a..c643d998 100644 --- a/testing/path/test_svnwc.py +++ b/testing/path/test_svnwc.py @@ -5,6 +5,12 @@ from py._path import svnwc as svncommon from svntestbase import CommonSvnTests + +pytestmark = pytest.mark.xfail(sys.platform.startswith('win'), + reason='#161 all tests in this file are failing on Windows', + run=False) + + def test_make_repo(path1, tmpdir): repo = tmpdir.join("repo") py.process.cmdexec('svnadmin create %s' % repo) @@ -106,15 +112,17 @@ def test_status_unchanged(self, path1): assert r.join('sampledir/otherfile').basename in [item.basename for item in s.unchanged] - @pytest.mark.xfail(reason="svn-1.7 has buggy 'status --xml' output") def test_status_update(self, path1): + # not a mark because the global "pytestmark" will end up overwriting a mark here + pytest.xfail("svn-1.7 has buggy 'status --xml' output") r = path1 try: r.update(rev=1) s = r.status(updates=1, rec=1) # Comparing just the file names, because paths are unpredictable # on Windows. (long vs. 8.3 paths) - py.std.pprint.pprint(s.allpath()) + import pprint + pprint.pprint(s.allpath()) assert r.join('anotherfile').basename in [item.basename for item in s.update_available] #assert len(s.update_available) == 1 diff --git a/testing/process/test_cmdexec.py b/testing/process/test_cmdexec.py index b539e0af..98463d90 100644 --- a/testing/process/test_cmdexec.py +++ b/testing/process/test_cmdexec.py @@ -2,7 +2,9 @@ from py.process import cmdexec def exvalue(): - return py.std.sys.exc_info()[1] + import sys + return sys.exc_info()[1] + class Test_exec_cmd: def test_simple(self): @@ -17,7 +19,7 @@ def test_simple_newline(self): assert py.builtin._istext(out) def test_simple_error(self): - py.test.raises (cmdexec.Error, cmdexec, 'exit 1') + py.test.raises(cmdexec.Error, cmdexec, 'exit 1') def test_simple_error_exact_status(self): try: diff --git a/testing/process/test_forkedfunc.py b/testing/process/test_forkedfunc.py index d4f9f985..ae0d9ab7 100644 --- a/testing/process/test_forkedfunc.py +++ b/testing/process/test_forkedfunc.py @@ -70,8 +70,6 @@ def boxf3(): def test_forkedfunc_signal(): result = py.process.ForkedFunc(boxseg).waitfinish() assert result.retval is None - if sys.version_info < (2,4): - py.test.skip("signal detection does not work with python prior 2.4") assert result.signal == 11 def test_forkedfunc_huge_data(): @@ -116,8 +114,6 @@ def box_fun(): ff = py.process.ForkedFunc(box_fun) os.kill(ff.pid, 15) result = ff.waitfinish() - if py.std.sys.version_info < (2,4): - py.test.skip("signal detection does not work with python prior 2.4") assert result.signal == 15 diff --git a/testing/process/test_killproc.py b/testing/process/test_killproc.py index 57088e1d..b0d6e2f5 100644 --- a/testing/process/test_killproc.py +++ b/testing/process/test_killproc.py @@ -1,16 +1,18 @@ +import pytest +import sys +import py -import py, sys -@py.test.mark.skipif("sys.platform.startswith('java')") +@pytest.mark.skipif("sys.platform.startswith('java')") def test_kill(tmpdir): - subprocess = py.test.importorskip("subprocess") + subprocess = pytest.importorskip("subprocess") t = tmpdir.join("t.py") t.write("import time ; time.sleep(100)") - proc = py.std.subprocess.Popen([sys.executable, str(t)]) - assert proc.poll() is None # no return value yet + proc = subprocess.Popen([sys.executable, str(t)]) + assert proc.poll() is None # no return value yet py.process.kill(proc.pid) ret = proc.wait() if sys.platform == "win32" and ret == 0: - py.test.skip("XXX on win32, subprocess.Popen().wait() on a killed " - "process does not yield return value != 0") + pytest.skip("XXX on win32, subprocess.Popen().wait() on a killed " + "process does not yield return value != 0") assert ret != 0 diff --git a/testing/root/test_builtin.py b/testing/root/test_builtin.py index a6f1a3c7..287c60d5 100644 --- a/testing/root/test_builtin.py +++ b/testing/root/test_builtin.py @@ -1,7 +1,7 @@ import sys import types import py -from py.builtin import set, frozenset, reversed, sorted +from py.builtin import set, frozenset def test_enumerate(): l = [0,1,2] @@ -53,29 +53,6 @@ def test_frozenset(): s = set([frozenset([0, 1]), frozenset([1, 0])]) assert len(s) == 1 -def test_sorted(): - if sorted == py.builtin.sorted: - return # don't test a real builtin - for s in [py.builtin.sorted]: - def test(): - assert s([3, 2, 1]) == [1, 2, 3] - assert s([1, 2, 3], reverse=True) == [3, 2, 1] - l = s([1, 2, 3, 4, 5, 6], key=lambda x: x % 2) - assert l == [2, 4, 6, 1, 3, 5] - l = s([1, 2, 3, 4], cmp=lambda x, y: -cmp(x, y)) - assert l == [4, 3, 2, 1] - l = s([1, 2, 3, 4], cmp=lambda x, y: -cmp(x, y), - key=lambda x: x % 2) - assert l == [1, 3, 2, 4] - - def compare(x, y): - assert type(x) == str - assert type(y) == str - return cmp(x, y) - data = 'The quick Brown fox Jumped over The lazy Dog'.split() - s(data, cmp=compare, key=str.lower) - yield test - def test_print_simple(): from py.builtin import print_ @@ -116,7 +93,7 @@ class A: def test_getfuncdict(): def f(): - pass + raise NotImplementedError f.x = 4 assert py.builtin._getfuncdict(f)["x"] == 4 assert py.builtin._getfuncdict(2) is None diff --git a/testing/root/test_error.py b/testing/root/test_error.py index 9982504d..7bfbef3b 100644 --- a/testing/root/test_error.py +++ b/testing/root/test_error.py @@ -2,6 +2,9 @@ import py import errno +import sys +import subprocess + def test_error_classes(): for name in errno.errorcode.values(): @@ -9,12 +12,19 @@ def test_error_classes(): assert issubclass(x, py.error.Error) assert issubclass(x, EnvironmentError) + +def test_has_name(): + assert py.error.__name__ == 'py.error' + + def test_picklability_issue1(): + import pickle e1 = py.error.ENOENT() - s = py.std.pickle.dumps(e1) - e2 = py.std.pickle.loads(s) + s = pickle.dumps(e1) + e2 = pickle.loads(s) assert isinstance(e2, py.error.ENOENT) + def test_unknown_error(): num = 3999 cls = py.error._geterrnoclass(num) @@ -24,7 +34,8 @@ def test_unknown_error(): cls2 = py.error._geterrnoclass(num) assert cls is cls2 -def test_error_conversion_ENOTDIR(testdir): + +def test_error_conversion_enotdir(testdir): p = testdir.makepyfile("") excinfo = py.test.raises(py.error.Error, py.error.checked_call, p.listdir) assert isinstance(excinfo.value, EnvironmentError) @@ -37,6 +48,12 @@ def test_checked_call_supports_kwargs(tmpdir): py.error.checked_call(tempfile.mkdtemp, dir=str(tmpdir)) +def test_error_importable(): + """Regression test for #179""" + subprocess.check_call( + [sys.executable, '-c', 'from py.error import ENOENT']) + + try: import unittest unittest.TestCase.assertWarns @@ -47,13 +64,13 @@ def test_checked_call_supports_kwargs(tmpdir): import warnings class Case(unittest.TestCase): - def test_assertWarns(self): + def test_assert_warns(self): # Clear everything "py.*" from sys.modules and re-import py # as a fresh start for mod in tuple(sys.modules.keys()): if mod and (mod == 'py' or mod.startswith('py.')): del sys.modules[mod] - import py + __import__('py') with self.assertWarns(UserWarning): warnings.warn('this should work') diff --git a/testing/root/test_py_imports.py b/testing/root/test_py_imports.py index 5f5954e9..31fe6ead 100644 --- a/testing/root/test_py_imports.py +++ b/testing/root/test_py_imports.py @@ -1,20 +1,17 @@ import py -import types import sys -def checksubpackage(name): + +@py.test.mark.parametrize('name', [x for x in dir(py) if x[0] != '_']) +def test_dir(name): obj = getattr(py, name) - if hasattr(obj, '__map__'): # isinstance(obj, Module): + if hasattr(obj, '__map__'): # isinstance(obj, Module): keys = dir(obj) assert len(keys) > 0 print (obj.__map__) for name in list(obj.__map__): assert hasattr(obj, name), (obj, name) -def test_dir(): - for name in dir(py): - if not name.startswith('_'): - yield checksubpackage, name def test_virtual_module_identity(): from py import path as path1 @@ -24,11 +21,12 @@ def test_virtual_module_identity(): from py.path import local as local2 assert local1 is local2 + def test_importall(): base = py._pydir nodirs = [ ] - if sys.version_info >= (3,0): + if sys.version_info >= (3, 0): nodirs.append(base.join('_code', '_assertionold.py')) else: nodirs.append(base.join('_code', '_assertionnew.py')) @@ -40,7 +38,7 @@ def recurse(p): if p.basename == '__init__.py': continue relpath = p.new(ext='').relto(base) - if base.sep in relpath: # not py/*.py itself + if base.sep in relpath: # not py/*.py itself for x in nodirs: if p == x or p.relto(x): break @@ -52,10 +50,16 @@ def recurse(p): except py.test.skip.Exception: pass + def check_import(modpath): py.builtin.print_("checking import", modpath) assert __import__(modpath) + +def test_star_import(): + exec("from py import *") + + def test_all_resolves(): seen = py.builtin.set([py]) lastlength = None @@ -65,4 +69,3 @@ def test_all_resolves(): for value in item.__dict__.values(): if isinstance(value, type(py.test)): seen.add(value) - diff --git a/testing/root/test_xmlgen.py b/testing/root/test_xmlgen.py index 704d1492..fc0e8266 100644 --- a/testing/root/test_xmlgen.py +++ b/testing/root/test_xmlgen.py @@ -1,6 +1,7 @@ import py from py._xmlgen import unicode, html, raw +import sys class ns(py.xml.Namespace): pass @@ -12,14 +13,14 @@ def __unicode__(self): return uvalue def __str__(self): x = self.__unicode__() - if py.std.sys.version_info[0] < 3: + if sys.version_info[0] < 3: return x.encode('utf-8') return x y = py.xml.escape(uvalue) assert y == uvalue x = py.xml.escape(A()) assert x == uvalue - if py.std.sys.version_info[0] < 3: + if sys.version_info[0] < 3: assert isinstance(x, unicode) assert isinstance(y, unicode) y = py.xml.escape(uvalue.encode('utf-8')) diff --git a/testing/test_iniconfig.py b/testing/test_iniconfig.py deleted file mode 100644 index 2bb573fa..00000000 --- a/testing/test_iniconfig.py +++ /dev/null @@ -1,299 +0,0 @@ -import py -import pytest -from py._iniconfig import IniConfig, ParseError, __all__ as ALL -from py._iniconfig import iscommentline -from textwrap import dedent - -def pytest_generate_tests(metafunc): - if 'input' in metafunc.funcargnames: - for name, (input, expected) in check_tokens.items(): - metafunc.addcall(id=name, funcargs={ - 'input': input, - 'expected': expected, - }) - elif hasattr(metafunc.function, 'multi'): - kwargs = metafunc.function.multi.kwargs - names, values = zip(*kwargs.items()) - values = cartesian_product(*values) - for p in values: - metafunc.addcall(funcargs=dict(zip(names, p))) - -def cartesian_product(L,*lists): - # copied from http://bit.ly/cyIXjn - if not lists: - for x in L: - yield (x,) - else: - for x in L: - for y in cartesian_product(lists[0],*lists[1:]): - yield (x,)+y - -check_tokens = { - 'section': ( - '[section]', - [(0, 'section', None, None)] - ), - 'value': ( - 'value = 1', - [(0, None, 'value', '1')] - ), - 'value in section': ( - '[section]\nvalue=1', - [(0, 'section', None, None), (1, 'section', 'value', '1')] - ), - 'value with continuation': ( - 'names =\n Alice\n Bob', - [(0, None, 'names', 'Alice\nBob')] - ), - 'value with aligned continuation': ( - 'names = Alice\n' - ' Bob', - [(0, None, 'names', 'Alice\nBob')] - ), - 'blank line':( - '[section]\n\nvalue=1', - [(0, 'section', None, None), (2, 'section', 'value', '1')] - ), - 'comment': ( - '# comment', - [] - ), - 'comment on value': ( - 'value = 1', - [(0, None, 'value', '1')] - ), - - 'comment on section': ( - '[section] #comment', - [(0, 'section', None, None)] - ), - 'comment2': ( - '; comment', - [] - ), - - 'comment2 on section': ( - '[section] ;comment', - [(0, 'section', None, None)] - ), - 'pseudo section syntax in value': ( - 'name = value []', - [(0, None, 'name', 'value []')] - ), - 'assignment in value': ( - 'value = x = 3', - [(0, None, 'value', 'x = 3')] - ), - 'use of colon for name-values': ( - 'name: y', - [(0, None, 'name', 'y')] - ), - 'use of colon without space': ( - 'value:y=5', - [(0, None, 'value', 'y=5')] - ), - 'equality gets precedence': ( - 'value=xyz:5', - [(0, None, 'value', 'xyz:5')] - ), - -} - -def parse(input): - # only for testing purposes - _parse() does not use state except path - ini = object.__new__(IniConfig) - ini.path = "sample" - return ini._parse(input.splitlines(True)) - -def parse_a_error(input): - return py.test.raises(ParseError, parse, input) - -def test_tokenize(input, expected): - parsed = parse(input) - assert parsed == expected - -def test_parse_empty(): - parsed = parse("") - assert not parsed - ini = IniConfig("sample", "") - assert not ini.sections - -def test_ParseError(): - e = ParseError("filename", 0, "hello") - assert str(e) == "filename:1: hello" - -def test_continuation_needs_perceeding_token(): - excinfo = parse_a_error(' Foo') - assert excinfo.value.lineno == 0 - -def test_continuation_cant_be_after_section(): - excinfo = parse_a_error('[section]\n Foo') - assert excinfo.value.lineno == 1 - -def test_section_cant_be_empty(): - excinfo = parse_a_error('[]') - -@py.test.mark.multi(line=[ - '!!', - ]) -def test_error_on_weird_lines(line): - parse_a_error(line) - -def test_iniconfig_from_file(tmpdir): - path = tmpdir/'test.txt' - path.write('[metadata]\nname=1') - - config = IniConfig(path=path) - assert list(config.sections) == ['metadata'] - config = IniConfig(path, "[diff]") - assert list(config.sections) == ['diff'] - py.test.raises(TypeError, "IniConfig(data=path.read())") - -def test_iniconfig_section_first(tmpdir): - excinfo = py.test.raises(ParseError, """ - IniConfig("x", data='name=1') - """) - assert "no section header defined" in str(excinfo.value) - -def test_iniconig_section_duplicate_fails(): - excinfo = py.test.raises(ParseError, r""" - IniConfig("x", data='[section]\n[section]') - """) - assert 'duplicate section' in str(excinfo.value) - -def test_iniconfig_duplicate_key_fails(): - excinfo = py.test.raises(ParseError, r""" - IniConfig("x", data='[section]\nname = Alice\nname = bob') - """) - - assert 'duplicate name' in str(excinfo.value) - -def test_iniconfig_lineof(): - config = IniConfig("x.ini", data= - '[section]\n' - 'value = 1\n' - '[section2]\n' - '# comment\n' - 'value =2' - ) - - assert config.lineof('missing') is None - assert config.lineof('section') == 1 - assert config.lineof('section2') == 3 - assert config.lineof('section', 'value') == 2 - assert config.lineof('section2','value') == 5 - - assert config['section'].lineof('value') == 2 - assert config['section2'].lineof('value') == 5 - -def test_iniconfig_get_convert(): - config= IniConfig("x", data='[section]\nint = 1\nfloat = 1.1') - assert config.get('section', 'int') == '1' - assert config.get('section', 'int', convert=int) == 1 - -def test_iniconfig_get_missing(): - config= IniConfig("x", data='[section]\nint = 1\nfloat = 1.1') - assert config.get('section', 'missing', default=1) == 1 - assert config.get('section', 'missing') is None - -def test_section_get(): - config = IniConfig("x", data='[section]\nvalue=1') - section = config['section'] - assert section.get('value', convert=int) == 1 - assert section.get('value', 1) == "1" - assert section.get('missing', 2) == 2 - -def test_missing_section(): - config = IniConfig("x", data='[section]\nvalue=1') - py.test.raises(KeyError,'config["other"]') - -def test_section_getitem(): - config = IniConfig("x", data='[section]\nvalue=1') - assert config['section']['value'] == '1' - assert config['section']['value'] == '1' - -def test_section_iter(): - config = IniConfig("x", data='[section]\nvalue=1') - names = list(config['section']) - assert names == ['value'] - items = list(config['section'].items()) - assert items==[('value', '1')] - -def test_config_iter(): - config = IniConfig("x.ini", data=dedent(''' - [section1] - value=1 - [section2] - value=2 - ''')) - l = list(config) - assert len(l) == 2 - assert l[0].name == 'section1' - assert l[0]['value'] == '1' - assert l[1].name == 'section2' - assert l[1]['value'] == '2' - -def test_config_contains(): - config = IniConfig("x.ini", data=dedent(''' - [section1] - value=1 - [section2] - value=2 - ''')) - assert 'xyz' not in config - assert 'section1' in config - assert 'section2' in config - -def test_iter_file_order(): - config = IniConfig("x.ini", data=""" -[section2] #cpython dict ordered before section -value = 1 -value2 = 2 # dict ordered before value -[section] -a = 1 -b = 2 -""") - l = list(config) - secnames = [x.name for x in l] - assert secnames == ['section2', 'section'] - assert list(config['section2']) == ['value', 'value2'] - assert list(config['section']) == ['a', 'b'] - -def test_example_pypirc(): - config = IniConfig("pypirc", data=dedent(''' - [distutils] - index-servers = - pypi - other - - [pypi] - repository: - username: - password: - - [other] - repository: http://example.com/pypi - username: - password: - ''')) - distutils, pypi, other = list(config) - assert distutils["index-servers"] == "pypi\nother" - assert pypi['repository'] == '' - assert pypi['username'] == '' - assert pypi['password'] == '' - assert ['repository', 'username', 'password'] == list(other) - - -def test_api_import(): - assert ALL == ['IniConfig', 'ParseError'] - -@pytest.mark.parametrize("line", [ - "#qwe", - " #qwe", - ";qwe", - " ;qwe", -]) -def test_iscommentline_true(line): - assert iscommentline(line) - - diff --git a/tox.ini b/tox.ini index 3b979b03..f3203507 100644 --- a/tox.ini +++ b/tox.ini @@ -1,25 +1,44 @@ [tox] -envlist=py26,py27,py33,py34,py35 +# Skip py37-pytest29 as such a combination does not work (#192) +envlist=py{27,35,36}-pytest{29,30,31},py37-pytest{30,31} [testenv] -changedir=testing -commands=py.test --confcutdir=.. -rfsxX --junitxml={envlogdir}/junit-{envname}.xml [] -deps=pytest~=2.9.0 +commands= + pip install -U . # hande the install order fallout since pytest depends on pip + py.test --confcutdir=. --junitxml={envlogdir}/junit-{envname}.xml [] +deps= + attrs + pytest29: pytest~=2.9.0 + pytest30: pytest~=3.0.0 + pytest31: pytest~=3.1.0 [testenv:py27-xdist] basepython=python2.7 deps= pytest~=2.9.0 - pytest-xdist + pytest-xdist<=1.16.0 commands= - py.test -n3 -rfsxX --confcutdir=.. --runslowtests \ + pip install -U .. # hande the install order fallout since pytest depends on pip + py.test -n3 --confcutdir=.. --runslowtests \ --junitxml={envlogdir}/junit-{envname}.xml [] [testenv:jython] changedir=testing commands= - {envpython} -m pytest --confcutdir=.. -rfsxX --junitxml={envlogdir}/junit-{envname}0.xml {posargs:io_ code} + {envpython} -m pip install -U .. # hande the install order fallout since pytest depends on pip + {envpython} -m pytest --confcutdir=.. --junitxml={envlogdir}/junit-{envname}0.xml {posargs:io_ code} [pytest] rsyncdirs = conftest.py py doc testing addopts = -ra +testpaths = testing + +[coverage:run] +branch = 1 +source = . +parallel = 1 +[coverage:report] +include = py/*,testing/* +exclude_lines = + #\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(cover|COVER) + ^\s*raise NotImplementedError\b