Skip to content

Commit 2027135

Browse files
committed
Merge pull request #1 from pjdelport/initial-version
Initial version
2 parents 58a69dc + ec06f5d commit 2027135

File tree

12 files changed

+567
-0
lines changed

12 files changed

+567
-0
lines changed

.coveragerc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# http://coverage.readthedocs.org/en/latest/config.html
2+
[run]
3+
branch = True
4+
5+
source =
6+
src
7+
tests

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,6 @@ dist/
1818
# Test coverage
1919
.coverage
2020
htmlcov
21+
22+
# Hypothesis examples
23+
.hypothesis/

.travis.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
language: python
2+
cache: pip
3+
4+
matrix:
5+
include:
6+
- { python: '2.7', env: TOXENV=py27 }
7+
- { python: '3.4', env: TOXENV=py34 }
8+
- { python: '3.5', env: TOXENV=py35 }
9+
- { python: 'pypy', env: TOXENV=pypy }
10+
- { python: 'pypy3', env: TOXENV=pypy3 }
11+
12+
# Report coverage for the latest Python 2 and 3 versions
13+
- { python: '2.7', env: TOXENV=py27-codecov }
14+
- { python: '3.5', env: TOXENV=py35-codecov }
15+
16+
allow_failures:
17+
# PyPy3 on Travis seems to be broken, as of 2016-02.
18+
#
19+
# See: https://github.com/travis-ci/travis-ci/issues/4306
20+
#
21+
- python: 'pypy3'
22+
23+
# Avoid overriding the default install step,
24+
# so that automatic pip caching works.
25+
#
26+
# See: https://github.com/travis-ci/travis-ci/issues/3239
27+
#
28+
before_script:
29+
- pip install tox
30+
31+
script:
32+
- tox

HACKING.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
=======================
2+
Working on backports.os
3+
=======================
4+
5+
6+
Running the tests
7+
=================
8+
9+
Running ``tox``, ``detox``, or ``pytest`` should all work.
10+
11+
With ``unittest``::
12+
13+
python -m unittest discover tests
14+
15+
16+
Coverage
17+
========
18+
19+
With ``coverage``::
20+
21+
coverage run -m unittest discover tests
22+
coverage report
23+
coverage html
24+
25+
With ``pytest`` and ``pytest-cov``::
26+
27+
py.test --cov
28+
py.test --cov --cov-report=html
29+

README.rst

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
============
2+
backports.os
3+
============
4+
5+
This package provides backports of new features in Python's os_ module
6+
under the backports_ namespace.
7+
8+
.. _os: https://docs.python.org/3.5/library/os.html
9+
.. _backports: https://pypi.python.org/pypi/backports
10+
11+
.. image:: https://img.shields.io/pypi/v/backports.os.svg
12+
:target: https://pypi.python.org/pypi/backports.os
13+
14+
.. image:: https://img.shields.io/badge/source-GitHub-lightgrey.svg
15+
:target: https://github.com/pjdelport/backports.os
16+
17+
.. image:: https://img.shields.io/github/issues/pjdelport/backports.os.svg
18+
:target: https://github.com/pjdelport/backports.os/issues?q=is:open
19+
20+
.. image:: https://travis-ci.org/pjdelport/backports.os.svg?branch=master
21+
:target: https://travis-ci.org/pjdelport/backports.os
22+
23+
.. image:: https://codecov.io/github/pjdelport/backports.os/coverage.svg?branch=master
24+
:target: https://codecov.io/github/pjdelport/backports.os?branch=master
25+
26+
27+
Supported Python versions
28+
=========================
29+
30+
* CPython: 2.7, 3.4, 3.5
31+
* PyPy
32+
33+
34+
Backported functionality
35+
========================
36+
37+
* `os.fsencode`_ (new in Python 3.2)
38+
* `os.fsdecode`_ (new in Python 3.2)
39+
40+
.. _`os.fsencode`: https://docs.python.org/3.5/library/os.html#os.fsencode
41+
.. _`os.fsdecode`: https://docs.python.org/3.5/library/os.html#os.fsdecode
42+
43+
44+
Contributing
45+
============
46+
47+
See `<HACKING.rst>`__.

setup.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# coding: utf-8
2+
import sys
3+
from setuptools import setup, find_packages
4+
5+
6+
# Backward-compatibility dependencies for Python 2
7+
_python2_requires = [
8+
'future', # For backport of surrogateescape
9+
] if sys.version_info < (3,) else []
10+
11+
12+
setup(
13+
name='backports.os',
14+
description="Backport of new features in Python's os module",
15+
url='https://github.com/pjdelport/backports.os',
16+
17+
author=u'Piët Delport',
18+
author_email='[email protected]',
19+
20+
package_dir={'': 'src'},
21+
packages=find_packages('src'),
22+
23+
setup_requires=['setuptools_scm'],
24+
use_scm_version=True,
25+
26+
install_requires=_python2_requires,
27+
28+
license='Python Software Foundation License',
29+
classifiers=[
30+
'Development Status :: 6 - Mature',
31+
'Intended Audience :: Developers',
32+
'License :: OSI Approved :: Python Software Foundation License',
33+
'Programming Language :: Python :: 2',
34+
'Programming Language :: Python :: 2.7',
35+
'Programming Language :: Python :: 3',
36+
'Programming Language :: Python :: 3.4',
37+
'Programming Language :: Python :: 3.5',
38+
'Topic :: Software Development :: Libraries :: Python Modules',
39+
],
40+
)

src/backports/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# See https://pypi.python.org/pypi/backports
2+
3+
from pkgutil import extend_path
4+
__path__ = extend_path(__path__, __name__)

src/backports/os.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
"""
2+
Partial backport of new functionality in Python 3.5's os module:
3+
4+
fsencode (new in Python 3.2)
5+
fsdecode (new in Python 3.2)
6+
7+
Backport modifications are marked with "XXX backport" and "TODO backport".
8+
"""
9+
from __future__ import unicode_literals
10+
11+
import sys
12+
13+
# XXX backport: unicode on Python 2
14+
_str = unicode if sys.version_info < (3,) else str
15+
16+
# XXX backport: Use backported surrogateescape for Python 2
17+
# TODO backport: Find a way to do this without pulling in the entire future package?
18+
if sys.version_info < (3,):
19+
from future.utils.surrogateescape import register_surrogateescape
20+
register_surrogateescape()
21+
22+
23+
# XXX backport: This invalid_utf8_indexes() helper is shamelessly copied from
24+
# Bob Ippolito's pyutf8 package (pyutf8/ref.py), in order to help support the
25+
# Python 2 UTF-8 decoding hack in fsdecode() below.
26+
#
27+
# URL: https://github.com/etrepum/pyutf8/blob/master/pyutf8/ref.py
28+
#
29+
def _invalid_utf8_indexes(bytes):
30+
skips = []
31+
i = 0
32+
len_bytes = len(bytes)
33+
while i < len_bytes:
34+
c1 = bytes[i]
35+
if c1 < 0x80:
36+
# U+0000 - U+007F - 7 bits
37+
i += 1
38+
continue
39+
try:
40+
c2 = bytes[i + 1]
41+
if ((c1 & 0xE0 == 0xC0) and (c2 & 0xC0 == 0x80)):
42+
# U+0080 - U+07FF - 11 bits
43+
c = (((c1 & 0x1F) << 6) |
44+
(c2 & 0x3F))
45+
if c < 0x80:
46+
# Overlong encoding
47+
skips.extend([i, i + 1])
48+
i += 2
49+
continue
50+
c3 = bytes[i + 2]
51+
if ((c1 & 0xF0 == 0xE0) and
52+
(c2 & 0xC0 == 0x80) and
53+
(c3 & 0xC0 == 0x80)):
54+
# U+0800 - U+FFFF - 16 bits
55+
c = (((((c1 & 0x0F) << 6) |
56+
(c2 & 0x3F)) << 6) |
57+
(c3 & 0x3f))
58+
if ((c < 0x800) or (0xD800 <= c <= 0xDFFF)):
59+
# Overlong encoding or surrogate.
60+
skips.extend([i, i + 1, i + 2])
61+
i += 3
62+
continue
63+
c4 = bytes[i + 3]
64+
if ((c1 & 0xF8 == 0xF0) and
65+
(c2 & 0xC0 == 0x80) and
66+
(c3 & 0xC0 == 0x80) and
67+
(c4 & 0xC0 == 0x80)):
68+
# U+10000 - U+10FFFF - 21 bits
69+
c = (((((((c1 & 0x0F) << 6) |
70+
(c2 & 0x3F)) << 6) |
71+
(c3 & 0x3F)) << 6) |
72+
(c4 & 0x3F))
73+
if (c < 0x10000) or (c > 0x10FFFF):
74+
# Overlong encoding or invalid code point.
75+
skips.extend([i, i + 1, i + 2, i + 3])
76+
i += 4
77+
continue
78+
except IndexError:
79+
pass
80+
skips.append(i)
81+
i += 1
82+
return skips
83+
84+
85+
# XXX backport: Another helper to support the Python 2 UTF-8 decoding hack.
86+
def _chunks(b, indexes):
87+
i = 0
88+
for j in indexes:
89+
yield b[i:j]
90+
yield b[j:j + 1]
91+
i = j + 1
92+
yield b[i:]
93+
94+
95+
def _fscodec():
96+
encoding = sys.getfilesystemencoding()
97+
if encoding == 'mbcs':
98+
errors = 'strict'
99+
else:
100+
errors = 'surrogateescape'
101+
102+
# XXX backport: Do we need to hack around Python 2's UTF-8 codec?
103+
import codecs # Use codecs.lookup() for name normalisation.
104+
_HACK_AROUND_PY2_UTF8 = (sys.version_info < (3,) and
105+
codecs.lookup(encoding) == codecs.lookup('utf-8'))
106+
107+
# XXX backport: chr(octet) became bytes([octet])
108+
_byte = chr if sys.version_info < (3,) else lambda i: bytes([i])
109+
110+
def fsencode(filename):
111+
"""
112+
Encode filename to the filesystem encoding with 'surrogateescape' error
113+
handler, return bytes unchanged. On Windows, use 'strict' error handler if
114+
the file system encoding is 'mbcs' (which is the default encoding).
115+
"""
116+
if isinstance(filename, bytes):
117+
return filename
118+
elif isinstance(filename, _str):
119+
if _HACK_AROUND_PY2_UTF8:
120+
# XXX backport: Unlike Python 3, Python 2's UTF-8 codec does not
121+
# consider surrogate codepoints invalid, so the surrogateescape
122+
# error handler never gets invoked to encode them back into high
123+
# bytes.
124+
#
125+
# This code hacks around that by manually encoding the surrogate
126+
# codepoints to high bytes, without relying on surrogateescape.
127+
#
128+
return b''.join(
129+
(_byte(ord(c) - 0xDC00) if 0xDC00 <= ord(c) <= 0xDCFF else
130+
c.encode(encoding))
131+
for c in filename)
132+
else:
133+
return filename.encode(encoding, errors)
134+
else:
135+
# XXX backport: unicode instead of str for Python 2
136+
raise TypeError("expect bytes or {_str}, not {}".format(type(filename).__name__,
137+
_str=_str.__name__, ))
138+
139+
def fsdecode(filename):
140+
"""
141+
Decode filename from the filesystem encoding with 'surrogateescape' error
142+
handler, return str unchanged. On Windows, use 'strict' error handler if
143+
the file system encoding is 'mbcs' (which is the default encoding).
144+
"""
145+
if isinstance(filename, _str):
146+
return filename
147+
elif isinstance(filename, bytes):
148+
if _HACK_AROUND_PY2_UTF8:
149+
# XXX backport: See the remarks in fsencode() above.
150+
#
151+
# This case is slightly trickier: Python 2 will invoke the
152+
# surrogateescape error handler for most bad high byte
153+
# sequences, *except* for full UTF-8 sequences that happen to
154+
# decode to surrogate codepoints.
155+
#
156+
# For decoding, it's not trivial to sidestep the UTF-8 codec
157+
# only for surrogates like fsencode() does, but as a hack we can
158+
# split the input into separate chunks around each invalid byte,
159+
# decode the chunks separately, and join the results.
160+
#
161+
# This prevents Python 2's UTF-8 codec from seeing the encoded
162+
# surrogate sequences as valid, which lets surrogateescape take
163+
# over and escape the individual bytes.
164+
#
165+
# TODO: Improve this.
166+
#
167+
from array import array
168+
indexes = _invalid_utf8_indexes(array(str('B'), filename))
169+
return ''.join(chunk.decode(encoding, errors)
170+
for chunk in _chunks(filename, indexes))
171+
else:
172+
return filename.decode(encoding, errors)
173+
else:
174+
# XXX backport: unicode instead of str for Python 2
175+
raise TypeError("expect bytes or {_str}, not {}".format(type(filename).__name__,
176+
_str=_str.__name__, ))
177+
178+
return fsencode, fsdecode
179+
180+
fsencode, fsdecode = _fscodec()
181+
del _fscodec

0 commit comments

Comments
 (0)