Merge pull request #153 from googlei18n/cython-setup
set up optional cython extension module
This commit is contained in:
commit
4ee6f7c15a
@ -6,6 +6,10 @@ branch = True
|
|||||||
# list of directories or packages to measure
|
# list of directories or packages to measure
|
||||||
source = cu2qu
|
source = cu2qu
|
||||||
|
|
||||||
|
# this is simply vendored, no need to include in coverage report
|
||||||
|
omit =
|
||||||
|
*/cu2qu/cython.py
|
||||||
|
|
||||||
# these are treated as equivalent when combining data
|
# these are treated as equivalent when combining data
|
||||||
[paths]
|
[paths]
|
||||||
source =
|
source =
|
||||||
|
10
.gitignore
vendored
10
.gitignore
vendored
@ -1,7 +1,12 @@
|
|||||||
# Byte-compiled and optimized files
|
# Byte-compiled and optimized files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[co]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# cython generated C/HTML files
|
||||||
|
Lib/cu2qu/*.c
|
||||||
|
Lib/cu2qu/*.html
|
||||||
|
|
||||||
# Packaging
|
# Packaging
|
||||||
*.egg-info
|
*.egg-info
|
||||||
@ -19,3 +24,6 @@ htmlcov
|
|||||||
|
|
||||||
# OS X Finder
|
# OS X Finder
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# auto-generated version file
|
||||||
|
Lib/cu2qu/_version.py
|
||||||
|
44
.travis.yml
44
.travis.yml
@ -1,29 +1,37 @@
|
|||||||
sudo: false
|
sudo: false
|
||||||
language: python
|
language: python
|
||||||
python:
|
|
||||||
- "2.7"
|
env:
|
||||||
- "3.6"
|
global:
|
||||||
|
- TWINE_USERNAME="anthrotype"
|
||||||
|
- secure: uIlWYz63F0Y/pDZawW2mS5DolWghdIodM8VtJtGbyIYB3fL3/edwTG5z+FWKaNeRtNfTAGDMs0y2dF45IJ7ZqTzw4yotgHz0uzLqjGjQ1/MOu99tppoXA6wQocZvmrdZpUcRbvBzJQzpAUcjldPLAP200mV9cG8+LfODn8Di2eJ329Ts3aA140pjUF8791jRLHBhUTpxK5RDPn1Q7OlugjryS7yNVIfT1/DaNDXAu4OZ8oNkygioRcyZ9QiFLjv5yBZ7uHB4UXWxw89RYPyz4NfHwyzDR38X/A6vfP19w2V0kecAK5BVBUE+WI7d26XjQzxDuH6Z0phB3x6MFuCXrX/pdrvNr7hs5kTAdQ7R1YA6MH4lPa+7oXha1/j353StzDMUKByXGVHyLAv7Ct2RSXOHUM6hAB4T+JbyJp/YkPWh968GKpl1lwvziKTi7K1qpngbXCdYIYKJ78IbmDcxzmQ+3j+fsXt9+gArZW7ICLgWrs+Lr1FiJsBOKLmqigOSmqrjHa+ef+wjieFSgzCVIfr9zvibzCEtYeqkuJuDQcSBIS0JG/heOfBGQ6FIxSXzwvICyWpldmfP67nBjVOVzPcyEAT8w+45LOM0HPCm6+Xjn7mKstc7x9TD7dVjWeyKJzZuKSmuAFA4UtGGKDQ93YQlAY2SCW4irYsusj80LHU=
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- env: TOXENV=py27-nocy
|
||||||
|
python: 2.7
|
||||||
|
- env: TOXENV=py36-nocy
|
||||||
|
python: 3.6
|
||||||
|
- env: TOXENV=py27-cy
|
||||||
|
python: 2.7
|
||||||
|
- env:
|
||||||
|
- TOXENV=py36-cy
|
||||||
|
- BUILD_DIST=true
|
||||||
|
python: 3.6
|
||||||
|
|
||||||
branches:
|
branches:
|
||||||
only:
|
only:
|
||||||
- master
|
- master
|
||||||
- /^v\d+\.\d+.*$/
|
- /^v\d+\.\d+.*$/
|
||||||
|
|
||||||
install: pip install tox-travis
|
install: pip install tox
|
||||||
|
|
||||||
script: tox
|
script: tox
|
||||||
|
|
||||||
after_success: tox -e codecov
|
after_success:
|
||||||
|
- if [ -z "$TRAVIS_TAG" ]; then tox -e codecov; fi
|
||||||
deploy:
|
|
||||||
# deploy to PyPI on tags
|
# deploy to PyPI on tags
|
||||||
provider: pypi
|
- |
|
||||||
server: https://upload.pypi.org/legacy/
|
if [ -n "$TRAVIS_TAG" ] && [ "$TRAVIS_REPO_SLUG" == "googlei18n/cu2qu" ] && [ "$BUILD_DIST" == true ]; then
|
||||||
on:
|
tox -e pypi
|
||||||
repo: googlei18n/cu2qu
|
fi
|
||||||
tags: true
|
|
||||||
all_branches: true
|
|
||||||
python: 3.6
|
|
||||||
user: anthrotype
|
|
||||||
password:
|
|
||||||
secure: LdEsho2OyiHKOsqjdwa1s6LQNuzbiHJGpk7L+Qn6XV8bmzznI15Z5yaC4kO8vE0ZfE0dwTcdA4BrjUBxFZnmWZtGp/la9pcwUF5fX8LnKwRsw8oPHJZNvvi9IEgnIg68VUJ4X787+hJKilQKhyE+J3UKDrJ0on6UJjpTciO8Tsins80EMD5wB000VriCXiZ3wvaCm/yaXDeGKkb8Us3NWT8pshgZ2SpoQyIJ1pik7p4UtjcZM2tPKbCPkim17UCOYQ/0II4KmoT19JGceC0xWbD0cssZDM2rd0vIQ+OMQTD7fkoTE2pY9L3dLDyHamJCECq/ZX0rNgFUBylEJq1+gin+8g81vXsewzBZA5Zc4/D+ER0INdLF8LbLAZOqu40eMa2X6bu3w++Vo8dK0wZibVlrA3EnBKcvTePTFuXnlAPuE252lsq+zn1nO8SO6xfzvB4JF3iB7GO7dajnR+8C+m9ctO/Lx043+FoxH607N1E/WOsFvOCPXQhpeNJZHyPtA4no++O0fp3KcKkvvJrt5nVxs55AT+p5uAXvfOATwwjaZYivayeZj2ICeFhh3A2LciMOvaqqXAJ64zFmjnmgVYyJPc7Xh30cnsvdKOR5QR35gzAZfEAyBeYIckIyIwpF9N+ogtGMQvHSprkbf1Bm6Hpmhq/XTkNzW+SM8+3/cfI=
|
|
||||||
distributions: sdist bdist_wheel
|
|
||||||
|
@ -15,350 +15,9 @@
|
|||||||
|
|
||||||
from __future__ import print_function, division, absolute_import
|
from __future__ import print_function, division, absolute_import
|
||||||
|
|
||||||
__version__ = "1.5.1.dev0"
|
|
||||||
|
|
||||||
__all__ = ['curve_to_quadratic', 'curves_to_quadratic']
|
|
||||||
|
|
||||||
MAX_N = 100
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import cython
|
from ._version import version as __version__
|
||||||
except:
|
except ImportError:
|
||||||
class Cython:
|
__version__ = "0.0.0+unknown"
|
||||||
@staticmethod
|
|
||||||
def func(*args, **kwargs):
|
|
||||||
return lambda x: x
|
|
||||||
@staticmethod
|
|
||||||
def cfunc(x): return x
|
|
||||||
def __getattr__(self, a):
|
|
||||||
return self.func
|
|
||||||
cython = Cython()
|
|
||||||
|
|
||||||
|
from .cu2qu import *
|
||||||
class Cu2QuError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ApproxNotFoundError(Cu2QuError):
|
|
||||||
def __init__(self, curve):
|
|
||||||
message = "no approximation found: %s" % curve
|
|
||||||
super(Cu2QuError, self).__init__(message)
|
|
||||||
self.curve = curve
|
|
||||||
|
|
||||||
@cython.cfunc
|
|
||||||
@cython.returns(cython.double)
|
|
||||||
@cython.locals(v1=cython.complex, v2=cython.complex)
|
|
||||||
def dot(v1, v2):
|
|
||||||
"""Return the dot product of two vectors."""
|
|
||||||
return (v1 * v2.conjugate()).real
|
|
||||||
|
|
||||||
|
|
||||||
@cython.cfunc
|
|
||||||
@cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex)
|
|
||||||
@cython.locals(_1=cython.complex, _2=cython.complex, _3=cython.complex, _4=cython.complex)
|
|
||||||
def calc_cubic_points(a, b, c, d):
|
|
||||||
_1 = d
|
|
||||||
_2 = (c * 0.3333333333333333) + d
|
|
||||||
_3 = (b + c) * 0.3333333333333333 + _2
|
|
||||||
_4 = a + d + c + b
|
|
||||||
return _1, _2, _3, _4
|
|
||||||
|
|
||||||
|
|
||||||
@cython.cfunc
|
|
||||||
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex)
|
|
||||||
@cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex)
|
|
||||||
def calc_cubic_parameters(p0, p1, p2, p3):
|
|
||||||
c = (p1 - p0) * 3.0
|
|
||||||
b = (p2 - p1) * 3.0 - c
|
|
||||||
d = p0
|
|
||||||
a = p3 - d - c - b
|
|
||||||
return a, b, c, d
|
|
||||||
|
|
||||||
|
|
||||||
@cython.cfunc
|
|
||||||
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex)
|
|
||||||
def split_cubic_into_n_iter(p0, p1, p2, p3, n):
|
|
||||||
# Hand-coded special-cases
|
|
||||||
if n == 2:
|
|
||||||
return iter(split_cubic_into_two(p0, p1, p2, p3))
|
|
||||||
if n == 3:
|
|
||||||
return iter(split_cubic_into_three(p0, p1, p2, p3))
|
|
||||||
if n == 4:
|
|
||||||
a, b = split_cubic_into_two(p0, p1, p2, p3)
|
|
||||||
return iter(split_cubic_into_two(*a) + split_cubic_into_two(*b))
|
|
||||||
if n == 6:
|
|
||||||
a, b = split_cubic_into_two(p0, p1, p2, p3)
|
|
||||||
return iter(split_cubic_into_three(*a) + split_cubic_into_three(*b))
|
|
||||||
|
|
||||||
return _split_cubic_into_n_gen(p0,p1,p2,p3,n)
|
|
||||||
|
|
||||||
|
|
||||||
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex, n=cython.int)
|
|
||||||
@cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex)
|
|
||||||
@cython.locals(dt=cython.double, delta_2=cython.double, delta_3=cython.double, i=cython.int)
|
|
||||||
@cython.locals(a1=cython.complex, b1=cython.complex, c1=cython.complex, d1=cython.complex)
|
|
||||||
def _split_cubic_into_n_gen(p0, p1, p2, p3, n):
|
|
||||||
a, b, c, d = calc_cubic_parameters(p0, p1, p2, p3)
|
|
||||||
dt = 1 / n
|
|
||||||
delta_2 = dt * dt
|
|
||||||
delta_3 = dt * delta_2
|
|
||||||
for i in range(n):
|
|
||||||
t1 = i * dt
|
|
||||||
t1_2 = t1 * t1
|
|
||||||
# calc new a, b, c and d
|
|
||||||
a1 = a * delta_3
|
|
||||||
b1 = (3*a*t1 + b) * delta_2
|
|
||||||
c1 = (2*b*t1 + c + 3*a*t1_2) * dt
|
|
||||||
d1 = a*t1*t1_2 + b*t1_2 + c*t1 + d
|
|
||||||
yield calc_cubic_points(a1, b1, c1, d1)
|
|
||||||
|
|
||||||
|
|
||||||
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex)
|
|
||||||
@cython.locals(mid=cython.complex, deriv3=cython.complex)
|
|
||||||
def split_cubic_into_two(p0, p1, p2, p3):
|
|
||||||
mid = (p0 + 3 * (p1 + p2) + p3) * .125
|
|
||||||
deriv3 = (p3 + p2 - p1 - p0) * .125
|
|
||||||
return ((p0, (p0 + p1) * .5, mid - deriv3, mid),
|
|
||||||
(mid, mid + deriv3, (p2 + p3) * .5, p3))
|
|
||||||
|
|
||||||
|
|
||||||
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex, _27=cython.double)
|
|
||||||
@cython.locals(mid1=cython.complex, deriv1=cython.complex, mid2=cython.complex, deriv2=cython.complex)
|
|
||||||
def split_cubic_into_three(p0, p1, p2, p3, _27=1/27):
|
|
||||||
# we define 1/27 as a keyword argument so that it will be evaluated only
|
|
||||||
# once but still in the scope of this function
|
|
||||||
mid1 = (8*p0 + 12*p1 + 6*p2 + p3) * _27
|
|
||||||
deriv1 = (p3 + 3*p2 - 4*p0) * _27
|
|
||||||
mid2 = (p0 + 6*p1 + 12*p2 + 8*p3) * _27
|
|
||||||
deriv2 = (4*p3 - 3*p1 - p0) * _27
|
|
||||||
return ((p0, (2*p0 + p1) * 0.3333333333333333, mid1 - deriv1, mid1),
|
|
||||||
(mid1, mid1 + deriv1, mid2 - deriv2, mid2),
|
|
||||||
(mid2, mid2 + deriv2, (p2 + 2*p3) * 0.3333333333333333, p3))
|
|
||||||
|
|
||||||
|
|
||||||
@cython.returns(cython.complex)
|
|
||||||
@cython.locals(t=cython.double, p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex)
|
|
||||||
@cython.locals(_p1=cython.complex, _p2=cython.complex)
|
|
||||||
def cubic_approx_control(t, p0, p1, p2, p3):
|
|
||||||
"""Approximate a cubic bezier curve with a quadratic one.
|
|
||||||
Returns the candidate control point."""
|
|
||||||
_p1 = p0 + (p1 - p0) * 1.5
|
|
||||||
_p2 = p3 + (p2 - p3) * 1.5
|
|
||||||
return _p1 + (_p2 - _p1) * t
|
|
||||||
|
|
||||||
|
|
||||||
@cython.returns(cython.complex)
|
|
||||||
@cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex)
|
|
||||||
@cython.locals(ab=cython.complex, cd=cython.complex, p=cython.complex, h=cython.double)
|
|
||||||
def calc_intersect(a, b, c, d):
|
|
||||||
"""Calculate the intersection of ab and cd, given a, b, c, d."""
|
|
||||||
|
|
||||||
ab = b - a
|
|
||||||
cd = d - c
|
|
||||||
p = ab * 1j
|
|
||||||
try:
|
|
||||||
h = dot(p, a - c) / dot(p, cd)
|
|
||||||
except ZeroDivisionError:
|
|
||||||
return None
|
|
||||||
return c + cd * h
|
|
||||||
|
|
||||||
|
|
||||||
@cython.cfunc
|
|
||||||
@cython.returns(cython.int)
|
|
||||||
@cython.locals(tolerance=cython.double, p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex)
|
|
||||||
@cython.locals(mid=cython.complex, deriv3=cython.complex)
|
|
||||||
def cubic_farthest_fit_inside(p0, p1, p2, p3, tolerance):
|
|
||||||
"""Returns True if the cubic Bezier p entirely lies within a distance
|
|
||||||
tolerance of origin, False otherwise. Assumes that p0 and p3 do fit
|
|
||||||
within tolerance of origin, and just checks the inside of the curve."""
|
|
||||||
|
|
||||||
# First check p2 then p1, as p2 has higher error early on.
|
|
||||||
if abs(p2) <= tolerance and abs(p1) <= tolerance:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Split.
|
|
||||||
mid = (p0 + 3 * (p1 + p2) + p3) * .125
|
|
||||||
if abs(mid) > tolerance:
|
|
||||||
return False
|
|
||||||
deriv3 = (p3 + p2 - p1 - p0) * .125
|
|
||||||
return (cubic_farthest_fit_inside(p0, (p0+p1)*.5, mid-deriv3, mid, tolerance) and
|
|
||||||
cubic_farthest_fit_inside(mid, mid+deriv3, (p2+p3)*.5, p3, tolerance))
|
|
||||||
|
|
||||||
|
|
||||||
@cython.cfunc
|
|
||||||
@cython.locals(tolerance=cython.double)
|
|
||||||
@cython.locals(q1=cython.complex, c0=cython.complex, c1=cython.complex, c2=cython.complex, c3=cython.complex)
|
|
||||||
def cubic_approx_quadratic(cubic, tolerance):
|
|
||||||
"""Return the uniq quadratic approximating cubic that maintains
|
|
||||||
endpoint tangents if that is within tolerance, None otherwise."""
|
|
||||||
# we define 2/3 as a keyword argument so that it will be evaluated only
|
|
||||||
# once but still in the scope of this function
|
|
||||||
|
|
||||||
q1 = calc_intersect(*cubic)
|
|
||||||
if q1 is None:
|
|
||||||
return None
|
|
||||||
c0 = cubic[0]
|
|
||||||
c3 = cubic[3]
|
|
||||||
c1 = c0 + (q1 - c0) * 0.6666666666666666
|
|
||||||
c2 = c3 + (q1 - c3) * 0.6666666666666666
|
|
||||||
if not cubic_farthest_fit_inside(0,
|
|
||||||
c1 - cubic[1],
|
|
||||||
c2 - cubic[2],
|
|
||||||
0, tolerance):
|
|
||||||
return None
|
|
||||||
return c0, q1, c3
|
|
||||||
|
|
||||||
|
|
||||||
@cython.cfunc
|
|
||||||
@cython.locals(n=cython.int, tolerance=cython.double)
|
|
||||||
@cython.locals(i=cython.int)
|
|
||||||
@cython.locals(c0=cython.complex, c1=cython.complex, c2=cython.complex, c3=cython.complex)
|
|
||||||
@cython.locals(q0=cython.complex, q1=cython.complex, next_q1=cython.complex, q2=cython.complex, d1=cython.complex)
|
|
||||||
def cubic_approx_spline(cubic, n, tolerance):
|
|
||||||
"""Approximate a cubic bezier curve with a spline of n quadratics.
|
|
||||||
|
|
||||||
Returns None if no quadratic approximation is found which lies entirely
|
|
||||||
within a distance `tolerance` from the original curve.
|
|
||||||
"""
|
|
||||||
# we define 2/3 as a keyword argument so that it will be evaluated only
|
|
||||||
# once but still in the scope of this function
|
|
||||||
|
|
||||||
if n == 1:
|
|
||||||
return cubic_approx_quadratic(cubic, tolerance)
|
|
||||||
|
|
||||||
cubics = split_cubic_into_n_iter(cubic[0], cubic[1], cubic[2], cubic[3], n)
|
|
||||||
|
|
||||||
# calculate the spline of quadratics and check errors at the same time.
|
|
||||||
next_cubic = next(cubics)
|
|
||||||
next_q1 = cubic_approx_control(0, *next_cubic)
|
|
||||||
q2 = cubic[0]
|
|
||||||
d1 = 0j
|
|
||||||
spline = [cubic[0], next_q1]
|
|
||||||
for i in range(1, n+1):
|
|
||||||
|
|
||||||
# Current cubic to convert
|
|
||||||
c0, c1, c2, c3 = next_cubic
|
|
||||||
|
|
||||||
# Current quadratic approximation of current cubic
|
|
||||||
q0 = q2
|
|
||||||
q1 = next_q1
|
|
||||||
if i < n:
|
|
||||||
next_cubic = next(cubics)
|
|
||||||
next_q1 = cubic_approx_control(i / (n-1), *next_cubic)
|
|
||||||
spline.append(next_q1)
|
|
||||||
q2 = (q1 + next_q1) * .5
|
|
||||||
else:
|
|
||||||
q2 = c3
|
|
||||||
|
|
||||||
# End-point deltas
|
|
||||||
d0 = d1
|
|
||||||
d1 = q2 - c3
|
|
||||||
|
|
||||||
if (abs(d1) > tolerance or
|
|
||||||
not cubic_farthest_fit_inside(d0,
|
|
||||||
q0 + (q1 - q0) * 0.6666666666666666 - c1,
|
|
||||||
q2 + (q1 - q2) * 0.6666666666666666 - c2,
|
|
||||||
d1,
|
|
||||||
tolerance)):
|
|
||||||
return None
|
|
||||||
spline.append(cubic[3])
|
|
||||||
|
|
||||||
return spline
|
|
||||||
|
|
||||||
|
|
||||||
@cython.locals(max_err=cython.double)
|
|
||||||
@cython.locals(n=cython.int)
|
|
||||||
def curve_to_quadratic(curve, max_err):
|
|
||||||
"""Return a quadratic spline approximating this cubic bezier.
|
|
||||||
Raise 'ApproxNotFoundError' if no suitable approximation can be found
|
|
||||||
with the given parameters.
|
|
||||||
"""
|
|
||||||
|
|
||||||
curve = [complex(*p) for p in curve]
|
|
||||||
|
|
||||||
for n in range(1, MAX_N + 1):
|
|
||||||
spline = cubic_approx_spline(curve, n, max_err)
|
|
||||||
if spline is not None:
|
|
||||||
# done. go home
|
|
||||||
return [(s.real, s.imag) for s in spline]
|
|
||||||
|
|
||||||
raise ApproxNotFoundError(curve)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@cython.locals(l=cython.int, last_i=cython.int, i=cython.int)
|
|
||||||
def curves_to_quadratic(curves, max_errors):
|
|
||||||
"""Return quadratic splines approximating these cubic beziers.
|
|
||||||
Raise 'ApproxNotFoundError' if no suitable approximation can be found
|
|
||||||
for all curves with the given parameters.
|
|
||||||
"""
|
|
||||||
|
|
||||||
curves = [[complex(*p) for p in curve] for curve in curves]
|
|
||||||
assert len(max_errors) == len(curves)
|
|
||||||
|
|
||||||
l = len(curves)
|
|
||||||
splines = [None] * l
|
|
||||||
last_i = i = 0
|
|
||||||
n = 1
|
|
||||||
while True:
|
|
||||||
spline = cubic_approx_spline(curves[i], n, max_errors[i])
|
|
||||||
if spline is None:
|
|
||||||
if n == MAX_N:
|
|
||||||
break
|
|
||||||
n += 1
|
|
||||||
last_i = i
|
|
||||||
continue
|
|
||||||
splines[i] = spline
|
|
||||||
i = (i + 1) % l
|
|
||||||
if i == last_i:
|
|
||||||
# done. go home
|
|
||||||
return [[(s.real, s.imag) for s in spline] for spline in splines]
|
|
||||||
|
|
||||||
raise ApproxNotFoundError(curves)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
import random
|
|
||||||
import timeit
|
|
||||||
|
|
||||||
MAX_ERR = 5
|
|
||||||
|
|
||||||
def generate_curve():
|
|
||||||
return [
|
|
||||||
tuple(float(random.randint(0, 2048)) for coord in range(2))
|
|
||||||
for point in range(4)]
|
|
||||||
|
|
||||||
def setup_curve_to_quadratic():
|
|
||||||
return generate_curve(), MAX_ERR
|
|
||||||
|
|
||||||
def setup_curves_to_quadratic():
|
|
||||||
num_curves = 3
|
|
||||||
return (
|
|
||||||
[generate_curve() for curve in range(num_curves)],
|
|
||||||
[MAX_ERR] * num_curves)
|
|
||||||
|
|
||||||
def run_benchmark(
|
|
||||||
benchmark_module, module, function, setup_suffix='', repeat=5, number=1000):
|
|
||||||
setup_func = 'setup_' + function
|
|
||||||
if setup_suffix:
|
|
||||||
print('%s with %s:' % (function, setup_suffix), end='')
|
|
||||||
setup_func += '_' + setup_suffix
|
|
||||||
else:
|
|
||||||
print('%s:' % function, end='')
|
|
||||||
|
|
||||||
def wrapper(function, setup_func):
|
|
||||||
function = globals()[function]
|
|
||||||
setup_func = globals()[setup_func]
|
|
||||||
def wrapped():
|
|
||||||
return function(*setup_func())
|
|
||||||
return wrapped
|
|
||||||
results = timeit.repeat(wrapper(function, setup_func), repeat=repeat, number=number)
|
|
||||||
print('\t%5.1fus' % (min(results) * 1000000. / number))
|
|
||||||
|
|
||||||
def main():
|
|
||||||
run_benchmark('cu2qu.benchmark', 'cu2qu', 'curve_to_quadratic')
|
|
||||||
run_benchmark('cu2qu.benchmark', 'cu2qu', 'curves_to_quadratic')
|
|
||||||
|
|
||||||
random.seed(1)
|
|
||||||
main()
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from __future__ import print_function, division, absolute_import
|
||||||
import os
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
370
Lib/cu2qu/cu2qu.py
Normal file
370
Lib/cu2qu/cu2qu.py
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
#cython: language_level=3
|
||||||
|
#distutils: define_macros=CYTHON_TRACE_NOGIL=1
|
||||||
|
|
||||||
|
# Copyright 2015 Google Inc. All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import print_function, division, absolute_import
|
||||||
|
|
||||||
|
try:
|
||||||
|
import cython
|
||||||
|
except ImportError:
|
||||||
|
# if not installed, use the embedded (no-op) copy of Cython.Shadow
|
||||||
|
from . import cython
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['curve_to_quadratic', 'curves_to_quadratic']
|
||||||
|
|
||||||
|
MAX_N = 100
|
||||||
|
|
||||||
|
NAN = float("NaN")
|
||||||
|
|
||||||
|
|
||||||
|
if cython.compiled:
|
||||||
|
# Yep, I'm compiled.
|
||||||
|
COMPILED = True
|
||||||
|
else:
|
||||||
|
# Just a lowly interpreted script.
|
||||||
|
COMPILED = False
|
||||||
|
|
||||||
|
|
||||||
|
class Cu2QuError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ApproxNotFoundError(Cu2QuError):
|
||||||
|
def __init__(self, curve):
|
||||||
|
message = "no approximation found: %s" % curve
|
||||||
|
super(Cu2QuError, self).__init__(message)
|
||||||
|
self.curve = curve
|
||||||
|
|
||||||
|
@cython.cfunc
|
||||||
|
@cython.returns(cython.double)
|
||||||
|
@cython.locals(v1=cython.complex, v2=cython.complex)
|
||||||
|
def dot(v1, v2):
|
||||||
|
"""Return the dot product of two vectors."""
|
||||||
|
return (v1 * v2.conjugate()).real
|
||||||
|
|
||||||
|
|
||||||
|
@cython.cfunc
|
||||||
|
@cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex)
|
||||||
|
@cython.locals(_1=cython.complex, _2=cython.complex, _3=cython.complex, _4=cython.complex)
|
||||||
|
def calc_cubic_points(a, b, c, d):
|
||||||
|
_1 = d
|
||||||
|
_2 = (c * 0.3333333333333333) + d
|
||||||
|
_3 = (b + c) * 0.3333333333333333 + _2
|
||||||
|
_4 = a + d + c + b
|
||||||
|
return _1, _2, _3, _4
|
||||||
|
|
||||||
|
|
||||||
|
@cython.cfunc
|
||||||
|
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex)
|
||||||
|
@cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex)
|
||||||
|
def calc_cubic_parameters(p0, p1, p2, p3):
|
||||||
|
c = (p1 - p0) * 3.0
|
||||||
|
b = (p2 - p1) * 3.0 - c
|
||||||
|
d = p0
|
||||||
|
a = p3 - d - c - b
|
||||||
|
return a, b, c, d
|
||||||
|
|
||||||
|
|
||||||
|
@cython.cfunc
|
||||||
|
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex)
|
||||||
|
def split_cubic_into_n_iter(p0, p1, p2, p3, n):
|
||||||
|
# Hand-coded special-cases
|
||||||
|
if n == 2:
|
||||||
|
return iter(split_cubic_into_two(p0, p1, p2, p3))
|
||||||
|
if n == 3:
|
||||||
|
return iter(split_cubic_into_three(p0, p1, p2, p3))
|
||||||
|
if n == 4:
|
||||||
|
a, b = split_cubic_into_two(p0, p1, p2, p3)
|
||||||
|
return iter(split_cubic_into_two(*a) + split_cubic_into_two(*b))
|
||||||
|
if n == 6:
|
||||||
|
a, b = split_cubic_into_two(p0, p1, p2, p3)
|
||||||
|
return iter(split_cubic_into_three(*a) + split_cubic_into_three(*b))
|
||||||
|
|
||||||
|
return _split_cubic_into_n_gen(p0,p1,p2,p3,n)
|
||||||
|
|
||||||
|
|
||||||
|
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex, n=cython.int)
|
||||||
|
@cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex)
|
||||||
|
@cython.locals(dt=cython.double, delta_2=cython.double, delta_3=cython.double, i=cython.int)
|
||||||
|
@cython.locals(a1=cython.complex, b1=cython.complex, c1=cython.complex, d1=cython.complex)
|
||||||
|
def _split_cubic_into_n_gen(p0, p1, p2, p3, n):
|
||||||
|
a, b, c, d = calc_cubic_parameters(p0, p1, p2, p3)
|
||||||
|
dt = 1 / n
|
||||||
|
delta_2 = dt * dt
|
||||||
|
delta_3 = dt * delta_2
|
||||||
|
for i in range(n):
|
||||||
|
t1 = i * dt
|
||||||
|
t1_2 = t1 * t1
|
||||||
|
# calc new a, b, c and d
|
||||||
|
a1 = a * delta_3
|
||||||
|
b1 = (3*a*t1 + b) * delta_2
|
||||||
|
c1 = (2*b*t1 + c + 3*a*t1_2) * dt
|
||||||
|
d1 = a*t1*t1_2 + b*t1_2 + c*t1 + d
|
||||||
|
yield calc_cubic_points(a1, b1, c1, d1)
|
||||||
|
|
||||||
|
|
||||||
|
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex)
|
||||||
|
@cython.locals(mid=cython.complex, deriv3=cython.complex)
|
||||||
|
def split_cubic_into_two(p0, p1, p2, p3):
|
||||||
|
mid = (p0 + 3 * (p1 + p2) + p3) * .125
|
||||||
|
deriv3 = (p3 + p2 - p1 - p0) * .125
|
||||||
|
return ((p0, (p0 + p1) * .5, mid - deriv3, mid),
|
||||||
|
(mid, mid + deriv3, (p2 + p3) * .5, p3))
|
||||||
|
|
||||||
|
|
||||||
|
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex, _27=cython.double)
|
||||||
|
@cython.locals(mid1=cython.complex, deriv1=cython.complex, mid2=cython.complex, deriv2=cython.complex)
|
||||||
|
def split_cubic_into_three(p0, p1, p2, p3, _27=1/27):
|
||||||
|
# we define 1/27 as a keyword argument so that it will be evaluated only
|
||||||
|
# once but still in the scope of this function
|
||||||
|
mid1 = (8*p0 + 12*p1 + 6*p2 + p3) * _27
|
||||||
|
deriv1 = (p3 + 3*p2 - 4*p0) * _27
|
||||||
|
mid2 = (p0 + 6*p1 + 12*p2 + 8*p3) * _27
|
||||||
|
deriv2 = (4*p3 - 3*p1 - p0) * _27
|
||||||
|
return ((p0, (2*p0 + p1) * 0.3333333333333333, mid1 - deriv1, mid1),
|
||||||
|
(mid1, mid1 + deriv1, mid2 - deriv2, mid2),
|
||||||
|
(mid2, mid2 + deriv2, (p2 + 2*p3) * 0.3333333333333333, p3))
|
||||||
|
|
||||||
|
|
||||||
|
@cython.returns(cython.complex)
|
||||||
|
@cython.locals(t=cython.double, p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex)
|
||||||
|
@cython.locals(_p1=cython.complex, _p2=cython.complex)
|
||||||
|
def cubic_approx_control(t, p0, p1, p2, p3):
|
||||||
|
"""Approximate a cubic bezier curve with a quadratic one.
|
||||||
|
Returns the candidate control point."""
|
||||||
|
_p1 = p0 + (p1 - p0) * 1.5
|
||||||
|
_p2 = p3 + (p2 - p3) * 1.5
|
||||||
|
return _p1 + (_p2 - _p1) * t
|
||||||
|
|
||||||
|
|
||||||
|
@cython.returns(cython.complex)
|
||||||
|
@cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex)
|
||||||
|
@cython.locals(ab=cython.complex, cd=cython.complex, p=cython.complex, h=cython.double)
|
||||||
|
def calc_intersect(a, b, c, d):
|
||||||
|
"""Calculate the intersection of ab and cd, given a, b, c, d."""
|
||||||
|
|
||||||
|
ab = b - a
|
||||||
|
cd = d - c
|
||||||
|
p = ab * 1j
|
||||||
|
try:
|
||||||
|
h = dot(p, a - c) / dot(p, cd)
|
||||||
|
except ZeroDivisionError:
|
||||||
|
return complex(NAN, NAN)
|
||||||
|
return c + cd * h
|
||||||
|
|
||||||
|
|
||||||
|
@cython.cfunc
|
||||||
|
@cython.returns(cython.int)
|
||||||
|
@cython.locals(tolerance=cython.double, p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex)
|
||||||
|
@cython.locals(mid=cython.complex, deriv3=cython.complex)
|
||||||
|
def cubic_farthest_fit_inside(p0, p1, p2, p3, tolerance):
|
||||||
|
"""Returns True if the cubic Bezier p entirely lies within a distance
|
||||||
|
tolerance of origin, False otherwise. Assumes that p0 and p3 do fit
|
||||||
|
within tolerance of origin, and just checks the inside of the curve."""
|
||||||
|
|
||||||
|
# First check p2 then p1, as p2 has higher error early on.
|
||||||
|
if abs(p2) <= tolerance and abs(p1) <= tolerance:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Split.
|
||||||
|
mid = (p0 + 3 * (p1 + p2) + p3) * .125
|
||||||
|
if abs(mid) > tolerance:
|
||||||
|
return False
|
||||||
|
deriv3 = (p3 + p2 - p1 - p0) * .125
|
||||||
|
return (cubic_farthest_fit_inside(p0, (p0+p1)*.5, mid-deriv3, mid, tolerance) and
|
||||||
|
cubic_farthest_fit_inside(mid, mid+deriv3, (p2+p3)*.5, p3, tolerance))
|
||||||
|
|
||||||
|
|
||||||
|
@cython.cfunc
|
||||||
|
@cython.locals(tolerance=cython.double)
|
||||||
|
@cython.locals(q1=cython.complex, c0=cython.complex, c1=cython.complex, c2=cython.complex, c3=cython.complex)
|
||||||
|
def cubic_approx_quadratic(cubic, tolerance):
|
||||||
|
"""Return the uniq quadratic approximating cubic that maintains
|
||||||
|
endpoint tangents if that is within tolerance, None otherwise."""
|
||||||
|
# we define 2/3 as a keyword argument so that it will be evaluated only
|
||||||
|
# once but still in the scope of this function
|
||||||
|
|
||||||
|
q1 = calc_intersect(*cubic)
|
||||||
|
if math.isnan(q1.imag):
|
||||||
|
return None
|
||||||
|
c0 = cubic[0]
|
||||||
|
c3 = cubic[3]
|
||||||
|
c1 = c0 + (q1 - c0) * 0.6666666666666666
|
||||||
|
c2 = c3 + (q1 - c3) * 0.6666666666666666
|
||||||
|
if not cubic_farthest_fit_inside(0,
|
||||||
|
c1 - cubic[1],
|
||||||
|
c2 - cubic[2],
|
||||||
|
0, tolerance):
|
||||||
|
return None
|
||||||
|
return c0, q1, c3
|
||||||
|
|
||||||
|
|
||||||
|
@cython.cfunc
|
||||||
|
@cython.locals(n=cython.int, tolerance=cython.double)
|
||||||
|
@cython.locals(i=cython.int)
|
||||||
|
@cython.locals(c0=cython.complex, c1=cython.complex, c2=cython.complex, c3=cython.complex)
|
||||||
|
@cython.locals(q0=cython.complex, q1=cython.complex, next_q1=cython.complex, q2=cython.complex, d1=cython.complex)
|
||||||
|
def cubic_approx_spline(cubic, n, tolerance):
|
||||||
|
"""Approximate a cubic bezier curve with a spline of n quadratics.
|
||||||
|
|
||||||
|
Returns None if no quadratic approximation is found which lies entirely
|
||||||
|
within a distance `tolerance` from the original curve.
|
||||||
|
"""
|
||||||
|
# we define 2/3 as a keyword argument so that it will be evaluated only
|
||||||
|
# once but still in the scope of this function
|
||||||
|
|
||||||
|
if n == 1:
|
||||||
|
return cubic_approx_quadratic(cubic, tolerance)
|
||||||
|
|
||||||
|
cubics = split_cubic_into_n_iter(cubic[0], cubic[1], cubic[2], cubic[3], n)
|
||||||
|
|
||||||
|
# calculate the spline of quadratics and check errors at the same time.
|
||||||
|
next_cubic = next(cubics)
|
||||||
|
next_q1 = cubic_approx_control(0, *next_cubic)
|
||||||
|
q2 = cubic[0]
|
||||||
|
d1 = 0j
|
||||||
|
spline = [cubic[0], next_q1]
|
||||||
|
for i in range(1, n+1):
|
||||||
|
|
||||||
|
# Current cubic to convert
|
||||||
|
c0, c1, c2, c3 = next_cubic
|
||||||
|
|
||||||
|
# Current quadratic approximation of current cubic
|
||||||
|
q0 = q2
|
||||||
|
q1 = next_q1
|
||||||
|
if i < n:
|
||||||
|
next_cubic = next(cubics)
|
||||||
|
next_q1 = cubic_approx_control(i / (n-1), *next_cubic)
|
||||||
|
spline.append(next_q1)
|
||||||
|
q2 = (q1 + next_q1) * .5
|
||||||
|
else:
|
||||||
|
q2 = c3
|
||||||
|
|
||||||
|
# End-point deltas
|
||||||
|
d0 = d1
|
||||||
|
d1 = q2 - c3
|
||||||
|
|
||||||
|
if (abs(d1) > tolerance or
|
||||||
|
not cubic_farthest_fit_inside(d0,
|
||||||
|
q0 + (q1 - q0) * 0.6666666666666666 - c1,
|
||||||
|
q2 + (q1 - q2) * 0.6666666666666666 - c2,
|
||||||
|
d1,
|
||||||
|
tolerance)):
|
||||||
|
return None
|
||||||
|
spline.append(cubic[3])
|
||||||
|
|
||||||
|
return spline
|
||||||
|
|
||||||
|
|
||||||
|
@cython.locals(max_err=cython.double)
|
||||||
|
@cython.locals(n=cython.int)
|
||||||
|
def curve_to_quadratic(curve, max_err):
|
||||||
|
"""Return a quadratic spline approximating this cubic bezier.
|
||||||
|
Raise 'ApproxNotFoundError' if no suitable approximation can be found
|
||||||
|
with the given parameters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
curve = [complex(*p) for p in curve]
|
||||||
|
|
||||||
|
for n in range(1, MAX_N + 1):
|
||||||
|
spline = cubic_approx_spline(curve, n, max_err)
|
||||||
|
if spline is not None:
|
||||||
|
# done. go home
|
||||||
|
return [(s.real, s.imag) for s in spline]
|
||||||
|
|
||||||
|
raise ApproxNotFoundError(curve)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@cython.locals(l=cython.int, last_i=cython.int, i=cython.int)
|
||||||
|
def curves_to_quadratic(curves, max_errors):
|
||||||
|
"""Return quadratic splines approximating these cubic beziers.
|
||||||
|
Raise 'ApproxNotFoundError' if no suitable approximation can be found
|
||||||
|
for all curves with the given parameters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
curves = [[complex(*p) for p in curve] for curve in curves]
|
||||||
|
assert len(max_errors) == len(curves)
|
||||||
|
|
||||||
|
l = len(curves)
|
||||||
|
splines = [None] * l
|
||||||
|
last_i = i = 0
|
||||||
|
n = 1
|
||||||
|
while True:
|
||||||
|
spline = cubic_approx_spline(curves[i], n, max_errors[i])
|
||||||
|
if spline is None:
|
||||||
|
if n == MAX_N:
|
||||||
|
break
|
||||||
|
n += 1
|
||||||
|
last_i = i
|
||||||
|
continue
|
||||||
|
splines[i] = spline
|
||||||
|
i = (i + 1) % l
|
||||||
|
if i == last_i:
|
||||||
|
# done. go home
|
||||||
|
return [[(s.real, s.imag) for s in spline] for spline in splines]
|
||||||
|
|
||||||
|
raise ApproxNotFoundError(curves)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import random
|
||||||
|
import timeit
|
||||||
|
|
||||||
|
MAX_ERR = 5
|
||||||
|
|
||||||
|
def generate_curve():
|
||||||
|
return [
|
||||||
|
tuple(float(random.randint(0, 2048)) for coord in range(2))
|
||||||
|
for point in range(4)]
|
||||||
|
|
||||||
|
def setup_curve_to_quadratic():
|
||||||
|
return generate_curve(), MAX_ERR
|
||||||
|
|
||||||
|
def setup_curves_to_quadratic():
|
||||||
|
num_curves = 3
|
||||||
|
return (
|
||||||
|
[generate_curve() for curve in range(num_curves)],
|
||||||
|
[MAX_ERR] * num_curves)
|
||||||
|
|
||||||
|
def run_benchmark(
|
||||||
|
benchmark_module, module, function, setup_suffix='', repeat=5, number=1000):
|
||||||
|
setup_func = 'setup_' + function
|
||||||
|
if setup_suffix:
|
||||||
|
print('%s with %s:' % (function, setup_suffix), end='')
|
||||||
|
setup_func += '_' + setup_suffix
|
||||||
|
else:
|
||||||
|
print('%s:' % function, end='')
|
||||||
|
|
||||||
|
def wrapper(function, setup_func):
|
||||||
|
function = globals()[function]
|
||||||
|
setup_func = globals()[setup_func]
|
||||||
|
def wrapped():
|
||||||
|
return function(*setup_func())
|
||||||
|
return wrapped
|
||||||
|
results = timeit.repeat(wrapper(function, setup_func), repeat=repeat, number=number)
|
||||||
|
print('\t%5.1fus' % (min(results) * 1000000. / number))
|
||||||
|
|
||||||
|
def main():
|
||||||
|
run_benchmark('cu2qu.benchmark', 'cu2qu', 'curve_to_quadratic')
|
||||||
|
run_benchmark('cu2qu.benchmark', 'cu2qu', 'curves_to_quadratic')
|
||||||
|
|
||||||
|
random.seed(1)
|
||||||
|
main()
|
468
Lib/cu2qu/cython.py
Normal file
468
Lib/cu2qu/cython.py
Normal file
@ -0,0 +1,468 @@
|
|||||||
|
""" This module is copied verbatim from the "Cython.Shadow" module:
|
||||||
|
https://github.com/cython/cython/blob/master/Cython/Shadow.py
|
||||||
|
|
||||||
|
Cython is licensed under the Apache 2.0 Software License.
|
||||||
|
"""
|
||||||
|
# cython.* namespace for pure mode.
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
__version__ = "0.28.5"
|
||||||
|
|
||||||
|
try:
|
||||||
|
from __builtin__ import basestring
|
||||||
|
except ImportError:
|
||||||
|
basestring = str
|
||||||
|
|
||||||
|
|
||||||
|
# BEGIN shameless copy from Cython/minivect/minitypes.py
|
||||||
|
|
||||||
|
class _ArrayType(object):
|
||||||
|
|
||||||
|
is_array = True
|
||||||
|
subtypes = ['dtype']
|
||||||
|
|
||||||
|
def __init__(self, dtype, ndim, is_c_contig=False, is_f_contig=False,
|
||||||
|
inner_contig=False, broadcasting=None):
|
||||||
|
self.dtype = dtype
|
||||||
|
self.ndim = ndim
|
||||||
|
self.is_c_contig = is_c_contig
|
||||||
|
self.is_f_contig = is_f_contig
|
||||||
|
self.inner_contig = inner_contig or is_c_contig or is_f_contig
|
||||||
|
self.broadcasting = broadcasting
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
axes = [":"] * self.ndim
|
||||||
|
if self.is_c_contig:
|
||||||
|
axes[-1] = "::1"
|
||||||
|
elif self.is_f_contig:
|
||||||
|
axes[0] = "::1"
|
||||||
|
|
||||||
|
return "%s[%s]" % (self.dtype, ", ".join(axes))
|
||||||
|
|
||||||
|
|
||||||
|
def index_type(base_type, item):
|
||||||
|
"""
|
||||||
|
Support array type creation by slicing, e.g. double[:, :] specifies
|
||||||
|
a 2D strided array of doubles. The syntax is the same as for
|
||||||
|
Cython memoryviews.
|
||||||
|
"""
|
||||||
|
class InvalidTypeSpecification(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def verify_slice(s):
|
||||||
|
if s.start or s.stop or s.step not in (None, 1):
|
||||||
|
raise InvalidTypeSpecification(
|
||||||
|
"Only a step of 1 may be provided to indicate C or "
|
||||||
|
"Fortran contiguity")
|
||||||
|
|
||||||
|
if isinstance(item, tuple):
|
||||||
|
step_idx = None
|
||||||
|
for idx, s in enumerate(item):
|
||||||
|
verify_slice(s)
|
||||||
|
if s.step and (step_idx or idx not in (0, len(item) - 1)):
|
||||||
|
raise InvalidTypeSpecification(
|
||||||
|
"Step may only be provided once, and only in the "
|
||||||
|
"first or last dimension.")
|
||||||
|
|
||||||
|
if s.step == 1:
|
||||||
|
step_idx = idx
|
||||||
|
|
||||||
|
return _ArrayType(base_type, len(item),
|
||||||
|
is_c_contig=step_idx == len(item) - 1,
|
||||||
|
is_f_contig=step_idx == 0)
|
||||||
|
elif isinstance(item, slice):
|
||||||
|
verify_slice(item)
|
||||||
|
return _ArrayType(base_type, 1, is_c_contig=bool(item.step))
|
||||||
|
else:
|
||||||
|
# int[8] etc.
|
||||||
|
assert int(item) == item # array size must be a plain integer
|
||||||
|
array(base_type, item)
|
||||||
|
|
||||||
|
# END shameless copy
|
||||||
|
|
||||||
|
|
||||||
|
compiled = False
|
||||||
|
|
||||||
|
_Unspecified = object()
|
||||||
|
|
||||||
|
# Function decorators
|
||||||
|
|
||||||
|
def _empty_decorator(x):
|
||||||
|
return x
|
||||||
|
|
||||||
|
def locals(**arg_types):
|
||||||
|
return _empty_decorator
|
||||||
|
|
||||||
|
def test_assert_path_exists(*paths):
|
||||||
|
return _empty_decorator
|
||||||
|
|
||||||
|
def test_fail_if_path_exists(*paths):
|
||||||
|
return _empty_decorator
|
||||||
|
|
||||||
|
class _EmptyDecoratorAndManager(object):
|
||||||
|
def __call__(self, x):
|
||||||
|
return x
|
||||||
|
def __enter__(self):
|
||||||
|
pass
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class _Optimization(object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
cclass = ccall = cfunc = _EmptyDecoratorAndManager()
|
||||||
|
|
||||||
|
returns = wraparound = boundscheck = initializedcheck = nonecheck = \
|
||||||
|
overflowcheck = embedsignature = cdivision = cdivision_warnings = \
|
||||||
|
always_allows_keywords = profile = linetrace = infer_types = \
|
||||||
|
unraisable_tracebacks = freelist = \
|
||||||
|
lambda _: _EmptyDecoratorAndManager()
|
||||||
|
|
||||||
|
exceptval = lambda _=None, check=True: _EmptyDecoratorAndManager()
|
||||||
|
|
||||||
|
optimization = _Optimization()
|
||||||
|
|
||||||
|
overflowcheck.fold = optimization.use_switch = \
|
||||||
|
optimization.unpack_method_calls = lambda arg: _EmptyDecoratorAndManager()
|
||||||
|
|
||||||
|
final = internal = type_version_tag = no_gc_clear = no_gc = _empty_decorator
|
||||||
|
|
||||||
|
|
||||||
|
_cython_inline = None
|
||||||
|
def inline(f, *args, **kwds):
|
||||||
|
if isinstance(f, basestring):
|
||||||
|
global _cython_inline
|
||||||
|
if _cython_inline is None:
|
||||||
|
from Cython.Build.Inline import cython_inline as _cython_inline
|
||||||
|
return _cython_inline(f, *args, **kwds)
|
||||||
|
else:
|
||||||
|
assert len(args) == len(kwds) == 0
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
def compile(f):
|
||||||
|
from Cython.Build.Inline import RuntimeCompiledFunction
|
||||||
|
return RuntimeCompiledFunction(f)
|
||||||
|
|
||||||
|
|
||||||
|
# Special functions
|
||||||
|
|
||||||
|
def cdiv(a, b):
|
||||||
|
q = a / b
|
||||||
|
if q < 0:
|
||||||
|
q += 1
|
||||||
|
return q
|
||||||
|
|
||||||
|
def cmod(a, b):
|
||||||
|
r = a % b
|
||||||
|
if (a*b) < 0:
|
||||||
|
r -= b
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
# Emulated language constructs
|
||||||
|
|
||||||
|
def cast(type, *args, **kwargs):
|
||||||
|
kwargs.pop('typecheck', None)
|
||||||
|
assert not kwargs
|
||||||
|
if hasattr(type, '__call__'):
|
||||||
|
return type(*args)
|
||||||
|
else:
|
||||||
|
return args[0]
|
||||||
|
|
||||||
|
def sizeof(arg):
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def typeof(arg):
|
||||||
|
return arg.__class__.__name__
|
||||||
|
# return type(arg)
|
||||||
|
|
||||||
|
def address(arg):
|
||||||
|
return pointer(type(arg))([arg])
|
||||||
|
|
||||||
|
def declare(type=None, value=_Unspecified, **kwds):
|
||||||
|
if type not in (None, object) and hasattr(type, '__call__'):
|
||||||
|
if value is not _Unspecified:
|
||||||
|
return type(value)
|
||||||
|
else:
|
||||||
|
return type()
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
class _nogil(object):
|
||||||
|
"""Support for 'with nogil' statement
|
||||||
|
"""
|
||||||
|
def __enter__(self):
|
||||||
|
pass
|
||||||
|
def __exit__(self, exc_class, exc, tb):
|
||||||
|
return exc_class is None
|
||||||
|
|
||||||
|
nogil = _nogil()
|
||||||
|
gil = _nogil()
|
||||||
|
del _nogil
|
||||||
|
|
||||||
|
# Emulated types
|
||||||
|
|
||||||
|
class CythonMetaType(type):
|
||||||
|
|
||||||
|
def __getitem__(type, ix):
|
||||||
|
return array(type, ix)
|
||||||
|
|
||||||
|
CythonTypeObject = CythonMetaType('CythonTypeObject', (object,), {})
|
||||||
|
|
||||||
|
class CythonType(CythonTypeObject):
|
||||||
|
|
||||||
|
def _pointer(self, n=1):
|
||||||
|
for i in range(n):
|
||||||
|
self = pointer(self)
|
||||||
|
return self
|
||||||
|
|
||||||
|
class PointerType(CythonType):
|
||||||
|
|
||||||
|
def __init__(self, value=None):
|
||||||
|
if isinstance(value, (ArrayType, PointerType)):
|
||||||
|
self._items = [cast(self._basetype, a) for a in value._items]
|
||||||
|
elif isinstance(value, list):
|
||||||
|
self._items = [cast(self._basetype, a) for a in value]
|
||||||
|
elif value is None or value == 0:
|
||||||
|
self._items = []
|
||||||
|
else:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
def __getitem__(self, ix):
|
||||||
|
if ix < 0:
|
||||||
|
raise IndexError("negative indexing not allowed in C")
|
||||||
|
return self._items[ix]
|
||||||
|
|
||||||
|
def __setitem__(self, ix, value):
|
||||||
|
if ix < 0:
|
||||||
|
raise IndexError("negative indexing not allowed in C")
|
||||||
|
self._items[ix] = cast(self._basetype, value)
|
||||||
|
|
||||||
|
def __eq__(self, value):
|
||||||
|
if value is None and not self._items:
|
||||||
|
return True
|
||||||
|
elif type(self) != type(value):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return not self._items and not value._items
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "%s *" % (self._basetype,)
|
||||||
|
|
||||||
|
class ArrayType(PointerType):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._items = [None] * self._n
|
||||||
|
|
||||||
|
|
||||||
|
class StructType(CythonType):
|
||||||
|
|
||||||
|
def __init__(self, cast_from=_Unspecified, **data):
|
||||||
|
if cast_from is not _Unspecified:
|
||||||
|
# do cast
|
||||||
|
if len(data) > 0:
|
||||||
|
raise ValueError('Cannot accept keyword arguments when casting.')
|
||||||
|
if type(cast_from) is not type(self):
|
||||||
|
raise ValueError('Cannot cast from %s'%cast_from)
|
||||||
|
for key, value in cast_from.__dict__.items():
|
||||||
|
setattr(self, key, value)
|
||||||
|
else:
|
||||||
|
for key, value in data.items():
|
||||||
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
def __setattr__(self, key, value):
|
||||||
|
if key in self._members:
|
||||||
|
self.__dict__[key] = cast(self._members[key], value)
|
||||||
|
else:
|
||||||
|
raise AttributeError("Struct has no member '%s'" % key)
|
||||||
|
|
||||||
|
|
||||||
|
class UnionType(CythonType):
|
||||||
|
|
||||||
|
def __init__(self, cast_from=_Unspecified, **data):
|
||||||
|
if cast_from is not _Unspecified:
|
||||||
|
# do type cast
|
||||||
|
if len(data) > 0:
|
||||||
|
raise ValueError('Cannot accept keyword arguments when casting.')
|
||||||
|
if isinstance(cast_from, dict):
|
||||||
|
datadict = cast_from
|
||||||
|
elif type(cast_from) is type(self):
|
||||||
|
datadict = cast_from.__dict__
|
||||||
|
else:
|
||||||
|
raise ValueError('Cannot cast from %s'%cast_from)
|
||||||
|
else:
|
||||||
|
datadict = data
|
||||||
|
if len(datadict) > 1:
|
||||||
|
raise AttributeError("Union can only store one field at a time.")
|
||||||
|
for key, value in datadict.items():
|
||||||
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
def __setattr__(self, key, value):
|
||||||
|
if key in '__dict__':
|
||||||
|
CythonType.__setattr__(self, key, value)
|
||||||
|
elif key in self._members:
|
||||||
|
self.__dict__ = {key: cast(self._members[key], value)}
|
||||||
|
else:
|
||||||
|
raise AttributeError("Union has no member '%s'" % key)
|
||||||
|
|
||||||
|
def pointer(basetype):
|
||||||
|
class PointerInstance(PointerType):
|
||||||
|
_basetype = basetype
|
||||||
|
return PointerInstance
|
||||||
|
|
||||||
|
def array(basetype, n):
|
||||||
|
class ArrayInstance(ArrayType):
|
||||||
|
_basetype = basetype
|
||||||
|
_n = n
|
||||||
|
return ArrayInstance
|
||||||
|
|
||||||
|
def struct(**members):
|
||||||
|
class StructInstance(StructType):
|
||||||
|
_members = members
|
||||||
|
for key in members:
|
||||||
|
setattr(StructInstance, key, None)
|
||||||
|
return StructInstance
|
||||||
|
|
||||||
|
def union(**members):
|
||||||
|
class UnionInstance(UnionType):
|
||||||
|
_members = members
|
||||||
|
for key in members:
|
||||||
|
setattr(UnionInstance, key, None)
|
||||||
|
return UnionInstance
|
||||||
|
|
||||||
|
class typedef(CythonType):
|
||||||
|
|
||||||
|
def __init__(self, type, name=None):
|
||||||
|
self._basetype = type
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def __call__(self, *arg):
|
||||||
|
value = cast(self._basetype, *arg)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.name or str(self._basetype)
|
||||||
|
|
||||||
|
__getitem__ = index_type
|
||||||
|
|
||||||
|
class _FusedType(CythonType):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def fused_type(*args):
|
||||||
|
if not args:
|
||||||
|
raise TypeError("Expected at least one type as argument")
|
||||||
|
|
||||||
|
# Find the numeric type with biggest rank if all types are numeric
|
||||||
|
rank = -1
|
||||||
|
for type in args:
|
||||||
|
if type not in (py_int, py_long, py_float, py_complex):
|
||||||
|
break
|
||||||
|
|
||||||
|
if type_ordering.index(type) > rank:
|
||||||
|
result_type = type
|
||||||
|
else:
|
||||||
|
return result_type
|
||||||
|
|
||||||
|
# Not a simple numeric type, return a fused type instance. The result
|
||||||
|
# isn't really meant to be used, as we can't keep track of the context in
|
||||||
|
# pure-mode. Casting won't do anything in this case.
|
||||||
|
return _FusedType()
|
||||||
|
|
||||||
|
|
||||||
|
def _specialized_from_args(signatures, args, kwargs):
|
||||||
|
"Perhaps this should be implemented in a TreeFragment in Cython code"
|
||||||
|
raise Exception("yet to be implemented")
|
||||||
|
|
||||||
|
|
||||||
|
py_int = typedef(int, "int")
|
||||||
|
try:
|
||||||
|
py_long = typedef(long, "long")
|
||||||
|
except NameError: # Py3
|
||||||
|
py_long = typedef(int, "long")
|
||||||
|
py_float = typedef(float, "float")
|
||||||
|
py_complex = typedef(complex, "double complex")
|
||||||
|
|
||||||
|
|
||||||
|
# Predefined types
|
||||||
|
|
||||||
|
int_types = ['char', 'short', 'Py_UNICODE', 'int', 'Py_UCS4', 'long', 'longlong', 'Py_ssize_t', 'size_t']
|
||||||
|
float_types = ['longdouble', 'double', 'float']
|
||||||
|
complex_types = ['longdoublecomplex', 'doublecomplex', 'floatcomplex', 'complex']
|
||||||
|
other_types = ['bint', 'void', 'Py_tss_t']
|
||||||
|
|
||||||
|
to_repr = {
|
||||||
|
'longlong': 'long long',
|
||||||
|
'longdouble': 'long double',
|
||||||
|
'longdoublecomplex': 'long double complex',
|
||||||
|
'doublecomplex': 'double complex',
|
||||||
|
'floatcomplex': 'float complex',
|
||||||
|
}.get
|
||||||
|
|
||||||
|
gs = globals()
|
||||||
|
|
||||||
|
# note: cannot simply name the unicode type here as 2to3 gets in the way and replaces it by str
|
||||||
|
try:
|
||||||
|
import __builtin__ as builtins
|
||||||
|
except ImportError: # Py3
|
||||||
|
import builtins
|
||||||
|
|
||||||
|
gs['unicode'] = typedef(getattr(builtins, 'unicode', str), 'unicode')
|
||||||
|
del builtins
|
||||||
|
|
||||||
|
for name in int_types:
|
||||||
|
reprname = to_repr(name, name)
|
||||||
|
gs[name] = typedef(py_int, reprname)
|
||||||
|
if name not in ('Py_UNICODE', 'Py_UCS4') and not name.endswith('size_t'):
|
||||||
|
gs['u'+name] = typedef(py_int, "unsigned " + reprname)
|
||||||
|
gs['s'+name] = typedef(py_int, "signed " + reprname)
|
||||||
|
|
||||||
|
for name in float_types:
|
||||||
|
gs[name] = typedef(py_float, to_repr(name, name))
|
||||||
|
|
||||||
|
for name in complex_types:
|
||||||
|
gs[name] = typedef(py_complex, to_repr(name, name))
|
||||||
|
|
||||||
|
bint = typedef(bool, "bint")
|
||||||
|
void = typedef(None, "void")
|
||||||
|
Py_tss_t = typedef(None, "Py_tss_t")
|
||||||
|
|
||||||
|
for t in int_types + float_types + complex_types + other_types:
|
||||||
|
for i in range(1, 4):
|
||||||
|
gs["%s_%s" % ('p'*i, t)] = gs[t]._pointer(i)
|
||||||
|
|
||||||
|
NULL = gs['p_void'](0)
|
||||||
|
|
||||||
|
# looks like 'gs' has some users out there by now...
|
||||||
|
#del gs
|
||||||
|
|
||||||
|
integral = floating = numeric = _FusedType()
|
||||||
|
|
||||||
|
type_ordering = [py_int, py_long, py_float, py_complex]
|
||||||
|
|
||||||
|
class CythonDotParallel(object):
|
||||||
|
"""
|
||||||
|
The cython.parallel module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__all__ = ['parallel', 'prange', 'threadid']
|
||||||
|
|
||||||
|
def parallel(self, num_threads=None):
|
||||||
|
return nogil
|
||||||
|
|
||||||
|
def prange(self, start=0, stop=None, step=1, schedule=None, nogil=False):
|
||||||
|
if stop is None:
|
||||||
|
stop = start
|
||||||
|
start = 0
|
||||||
|
return range(start, stop, step)
|
||||||
|
|
||||||
|
def threadid(self):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# def threadsavailable(self):
|
||||||
|
# return 1
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.modules['cython.parallel'] = CythonDotParallel()
|
||||||
|
del sys
|
35
setup.cfg
35
setup.cfg
@ -1,29 +1,3 @@
|
|||||||
[bumpversion]
|
|
||||||
current_version = 1.5.1.dev0
|
|
||||||
commit = True
|
|
||||||
tag = False
|
|
||||||
tag_name = v{new_version}
|
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\.(?P<release>[a-z]+)(?P<dev>\d+))?
|
|
||||||
serialize =
|
|
||||||
{major}.{minor}.{patch}.{release}{dev}
|
|
||||||
{major}.{minor}.{patch}
|
|
||||||
|
|
||||||
[bumpversion:part:release]
|
|
||||||
optional_value = final
|
|
||||||
values =
|
|
||||||
dev
|
|
||||||
final
|
|
||||||
|
|
||||||
[bumpversion:part:dev]
|
|
||||||
|
|
||||||
[bumpversion:file:Lib/cu2qu/__init__.py]
|
|
||||||
search = __version__ = "{current_version}"
|
|
||||||
replace = __version__ = "{new_version}"
|
|
||||||
|
|
||||||
[bumpversion:file:setup.py]
|
|
||||||
search = version="{current_version}"
|
|
||||||
replace = version="{new_version}"
|
|
||||||
|
|
||||||
[wheel]
|
[wheel]
|
||||||
universal = 1
|
universal = 1
|
||||||
|
|
||||||
@ -38,16 +12,15 @@ license_file = LICENSE
|
|||||||
|
|
||||||
[tool:pytest]
|
[tool:pytest]
|
||||||
minversion = 3.0
|
minversion = 3.0
|
||||||
testpaths =
|
testpaths =
|
||||||
tests
|
tests
|
||||||
python_files =
|
python_files =
|
||||||
*_test.py
|
*_test.py
|
||||||
python_classes =
|
python_classes =
|
||||||
*Test
|
*Test
|
||||||
addopts =
|
addopts =
|
||||||
-s
|
-s
|
||||||
-v
|
-v
|
||||||
-r a
|
-r a
|
||||||
--doctest-modules
|
--doctest-modules
|
||||||
--doctest-ignore-import-errors
|
--doctest-ignore-import-errors
|
||||||
|
|
||||||
|
320
setup.py
320
setup.py
@ -13,159 +13,191 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
from setuptools import setup, find_packages, Command
|
from setuptools import setup, find_packages, Extension
|
||||||
import sys
|
from setuptools.command.build_ext import build_ext as _build_ext
|
||||||
|
from setuptools.command.sdist import sdist as _sdist
|
||||||
|
import pkg_resources
|
||||||
from distutils import log
|
from distutils import log
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
class bump_version(Command):
|
import re
|
||||||
|
from io import open
|
||||||
description = "increment the package version and commit the changes"
|
|
||||||
|
|
||||||
user_options = [
|
|
||||||
("major", None, "bump the first digit, for incompatible API changes"),
|
|
||||||
("minor", None, "bump the second digit, for new backward-compatible features"),
|
|
||||||
("patch", None, "bump the third digit, for bug fixes (default)"),
|
|
||||||
]
|
|
||||||
|
|
||||||
def initialize_options(self):
|
|
||||||
self.minor = False
|
|
||||||
self.major = False
|
|
||||||
self.patch = False
|
|
||||||
|
|
||||||
def finalize_options(self):
|
|
||||||
part = None
|
|
||||||
for attr in ("major", "minor", "patch"):
|
|
||||||
if getattr(self, attr, False):
|
|
||||||
if part is None:
|
|
||||||
part = attr
|
|
||||||
else:
|
|
||||||
from distutils.errors import DistutilsOptionError
|
|
||||||
raise DistutilsOptionError(
|
|
||||||
"version part options are mutually exclusive")
|
|
||||||
self.part = part or "patch"
|
|
||||||
|
|
||||||
def bumpversion(self, part, **kwargs):
|
|
||||||
""" Run bumpversion.main() with the specified arguments.
|
|
||||||
"""
|
|
||||||
import bumpversion
|
|
||||||
|
|
||||||
args = ['--verbose'] if self.verbose > 1 else []
|
|
||||||
for k, v in kwargs.items():
|
|
||||||
arg = "--{}".format(k.replace("_", "-"))
|
|
||||||
if isinstance(v, bool):
|
|
||||||
if v is False:
|
|
||||||
continue
|
|
||||||
args.append(arg)
|
|
||||||
else:
|
|
||||||
args.extend([arg, str(v)])
|
|
||||||
args.append(part)
|
|
||||||
|
|
||||||
log.debug(
|
|
||||||
"$ bumpversion %s" % " ".join(a.replace(" ", "\\ ") for a in args))
|
|
||||||
|
|
||||||
bumpversion.main(args)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
log.info("bumping '%s' version" % self.part)
|
|
||||||
self.bumpversion(self.part)
|
|
||||||
|
|
||||||
|
|
||||||
class release(bump_version):
|
|
||||||
"""Drop the developmental release '.devN' suffix from the package version,
|
|
||||||
open the default text $EDITOR to write release notes, commit the changes
|
|
||||||
and generate a git tag.
|
|
||||||
|
|
||||||
Release notes can also be set with the -m/--message option, or by reading
|
|
||||||
from standard input.
|
|
||||||
"""
|
|
||||||
|
|
||||||
description = "tag a new release"
|
|
||||||
|
|
||||||
user_options = [
|
|
||||||
("message=", 'm', "message containing the release notes"),
|
|
||||||
("sign", "s", "make a GPG-signed tag, using the default key"),
|
|
||||||
]
|
|
||||||
|
|
||||||
def initialize_options(self):
|
|
||||||
self.message = None
|
|
||||||
self.sign = False
|
|
||||||
|
|
||||||
def finalize_options(self):
|
|
||||||
import re
|
|
||||||
|
|
||||||
current_version = self.distribution.metadata.get_version()
|
|
||||||
if not re.search(r"\.dev[0-9]+", current_version):
|
|
||||||
from distutils.errors import DistutilsSetupError
|
|
||||||
raise DistutilsSetupError(
|
|
||||||
"current version (%s) has no '.devN' suffix.\n "
|
|
||||||
"Run 'setup.py bump_version' with any of "
|
|
||||||
"--major, --minor, --patch options" % current_version)
|
|
||||||
|
|
||||||
message = self.message
|
|
||||||
if message is None:
|
|
||||||
if sys.stdin.isatty():
|
|
||||||
# stdin is interactive, use editor to write release notes
|
|
||||||
message = self.edit_release_notes()
|
|
||||||
else:
|
|
||||||
# read release notes from stdin pipe
|
|
||||||
message = sys.stdin.read()
|
|
||||||
|
|
||||||
if not message.strip():
|
|
||||||
from distutils.errors import DistutilsSetupError
|
|
||||||
raise DistutilsSetupError("release notes message is empty")
|
|
||||||
|
|
||||||
self.message = "v{new_version}\n\n%s" % message
|
|
||||||
self.sign = bool(self.sign)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def edit_release_notes():
|
|
||||||
"""Use the default text $EDITOR to write release notes.
|
|
||||||
If $EDITOR is not set, use 'nano'."""
|
|
||||||
from tempfile import mkstemp
|
|
||||||
import os
|
|
||||||
import shlex
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
text_editor = shlex.split(os.environ.get('EDITOR', 'nano'))
|
|
||||||
|
|
||||||
fd, tmp = mkstemp(prefix='bumpversion-')
|
|
||||||
try:
|
|
||||||
os.close(fd)
|
|
||||||
with open(tmp, 'w') as f:
|
|
||||||
f.write("\n\n# Write release notes.\n"
|
|
||||||
"# Lines starting with '#' will be ignored.")
|
|
||||||
subprocess.check_call(text_editor + [tmp])
|
|
||||||
with open(tmp, 'r') as f:
|
|
||||||
changes = "".join(
|
|
||||||
l for l in f.readlines() if not l.startswith('#'))
|
|
||||||
finally:
|
|
||||||
os.remove(tmp)
|
|
||||||
return changes
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
log.info("stripping developmental release suffix")
|
|
||||||
# drop '.dev0' suffix, commit with given message and create git tag
|
|
||||||
self.bumpversion("release",
|
|
||||||
tag=True,
|
|
||||||
message="Release {new_version}",
|
|
||||||
tag_message=self.message,
|
|
||||||
sign_tags=self.sign)
|
|
||||||
|
|
||||||
|
|
||||||
needs_pytest = {'pytest', 'test'}.intersection(sys.argv)
|
needs_pytest = {'pytest', 'test'}.intersection(sys.argv)
|
||||||
pytest_runner = ['pytest_runner'] if needs_pytest else []
|
pytest_runner = ['pytest_runner'] if needs_pytest else []
|
||||||
needs_wheel = {'bdist_wheel'}.intersection(sys.argv)
|
needs_wheel = {'bdist_wheel'}.intersection(sys.argv)
|
||||||
wheel = ['wheel'] if needs_wheel else []
|
wheel = ['wheel'] if needs_wheel else []
|
||||||
needs_bump2version = {'release', 'bump_version'}.intersection(sys.argv)
|
|
||||||
bump2version = ['bump2version >= 0.5.7'] if needs_bump2version else []
|
# Check if minimum required Cython is available.
|
||||||
|
# For consistency, we require the same as our vendored Cython.Shadow module
|
||||||
|
cymod = "Lib/cu2qu/cython.py"
|
||||||
|
cython_version_re = re.compile('__version__ = ["\']([0-9][0-9\w\.]+)["\']')
|
||||||
|
with open(cymod, "r", encoding="utf-8") as fp:
|
||||||
|
for line in fp:
|
||||||
|
m = cython_version_re.match(line)
|
||||||
|
if m:
|
||||||
|
cython_min_version = m.group(1)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
sys.exit("error: failed to parse cython version in '%s'" % cymod)
|
||||||
|
|
||||||
|
required_cython = "cython >= %s" % cython_min_version
|
||||||
|
try:
|
||||||
|
pkg_resources.require(required_cython)
|
||||||
|
except pkg_resources.ResolutionError:
|
||||||
|
has_cython = False
|
||||||
|
else:
|
||||||
|
has_cython = True
|
||||||
|
|
||||||
|
# First, check if the CU2QU_WITH_CYTHON environment variable is set.
|
||||||
|
# Values "1", "true" or "yes" mean that Cython is required and will be used
|
||||||
|
# to regenerate the *.c sources from which the native extension is built;
|
||||||
|
# "0", "false" or "no" mean that Cython is not required and no extension
|
||||||
|
# module will be compiled (i.e. the wheel is pure-python and universal).
|
||||||
|
# If the variable is not set, then the pre-generated *.c sources that
|
||||||
|
# are included in the sdist package will be used to try build the extension.
|
||||||
|
# However, if any error occurs during compilation (e.g. the host
|
||||||
|
# machine doesn't have the required compiler toolchain installed), the
|
||||||
|
# installation proceeds without the compiled extensions, but will only have
|
||||||
|
# the pure-python module.
|
||||||
|
env_with_cython = os.environ.get("CU2QU_WITH_CYTHON")
|
||||||
|
with_cython = (
|
||||||
|
True if env_with_cython in {"1", "true", "yes"}
|
||||||
|
else False if env_with_cython in {"0", "false", "no"}
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# command line options --with-cython and --without-cython are also supported.
|
||||||
|
# They override the environment variable
|
||||||
|
opt_with_cython = {'--with-cython'}.intersection(sys.argv)
|
||||||
|
opt_without_cython = {'--without-cython'}.intersection(sys.argv)
|
||||||
|
if opt_with_cython and opt_without_cython:
|
||||||
|
sys.exit(
|
||||||
|
"error: the options '--with-cython' and '--without-cython' are "
|
||||||
|
"mutually exclusive"
|
||||||
|
)
|
||||||
|
elif opt_with_cython:
|
||||||
|
sys.argv.remove("--with-cython")
|
||||||
|
with_cython = True
|
||||||
|
elif opt_without_cython:
|
||||||
|
sys.argv.remove("--without-cython")
|
||||||
|
with_cython = False
|
||||||
|
|
||||||
|
|
||||||
|
class cython_build_ext(_build_ext):
|
||||||
|
"""Compile *.pyx source files to *.c using cythonize if Cython is
|
||||||
|
installed, else use the pre-generated *.c sources.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def finalize_options(self):
|
||||||
|
if with_cython:
|
||||||
|
if not has_cython:
|
||||||
|
from distutils.errors import DistutilsSetupError
|
||||||
|
|
||||||
|
raise DistutilsSetupError(
|
||||||
|
"%s is required when using --with-cython" % required_cython
|
||||||
|
)
|
||||||
|
|
||||||
|
from Cython.Build import cythonize
|
||||||
|
|
||||||
|
# optionally enable line tracing for test coverage support
|
||||||
|
linetrace = os.environ.get("CYTHON_TRACE") == "1"
|
||||||
|
|
||||||
|
self.distribution.ext_modules[:] = cythonize(
|
||||||
|
self.distribution.ext_modules,
|
||||||
|
force=linetrace or self.force,
|
||||||
|
annotate=os.environ.get("CYTHON_ANNOTATE") == "1",
|
||||||
|
quiet=not self.verbose,
|
||||||
|
compiler_directives={
|
||||||
|
"linetrace": linetrace,
|
||||||
|
"language_level": 3,
|
||||||
|
"embedsignature": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# replace *.py/.pyx sources with their pre-generated *.c versions
|
||||||
|
for ext in self.distribution.ext_modules:
|
||||||
|
ext.sources = [re.sub("\.pyx?$", ".c", n) for n in ext.sources]
|
||||||
|
|
||||||
|
_build_ext.finalize_options(self)
|
||||||
|
|
||||||
|
def build_extensions(self):
|
||||||
|
if not has_cython:
|
||||||
|
log.info(
|
||||||
|
"%s is not installed. Pre-generated *.c sources will be "
|
||||||
|
"will be used to build the extensions." % required_cython
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
_build_ext.build_extensions(self)
|
||||||
|
except Exception as e:
|
||||||
|
if with_cython:
|
||||||
|
raise
|
||||||
|
# optional compilation failed: we delete 'ext_modules' and make sure
|
||||||
|
# the generated wheel is 'pure'
|
||||||
|
del self.distribution.ext_modules[:]
|
||||||
|
bdist_wheel = self.get_finalized_command("bdist_wheel")
|
||||||
|
bdist_wheel.root_is_pure = True
|
||||||
|
log.error('error: building extensions failed: %s' % e)
|
||||||
|
|
||||||
|
def get_source_files(self):
|
||||||
|
filenames = _build_ext.get_source_files(self)
|
||||||
|
|
||||||
|
# include pre-generated *.c sources inside sdist, but only if cython is
|
||||||
|
# installed (and hence they will be updated upon making the sdist)
|
||||||
|
if has_cython:
|
||||||
|
for ext in self.extensions:
|
||||||
|
filenames.extend(
|
||||||
|
[re.sub("\.pyx?$", ".c", n) for n in ext.sources]
|
||||||
|
)
|
||||||
|
return filenames
|
||||||
|
|
||||||
|
|
||||||
|
class cython_sdist(_sdist):
|
||||||
|
""" Run 'cythonize' on *.pyx sources to ensure the *.c files included
|
||||||
|
in the source distribution are up-to-date.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if with_cython and not has_cython:
|
||||||
|
from distutils.errors import DistutilsSetupError
|
||||||
|
|
||||||
|
raise DistutilsSetupError(
|
||||||
|
"%s is required when creating sdist --with-cython"
|
||||||
|
% required_cython
|
||||||
|
)
|
||||||
|
|
||||||
|
if has_cython:
|
||||||
|
from Cython.Build import cythonize
|
||||||
|
|
||||||
|
cythonize(
|
||||||
|
self.distribution.ext_modules,
|
||||||
|
force=True, # always regenerate *.c sources
|
||||||
|
quiet=not self.verbose,
|
||||||
|
compiler_directives={
|
||||||
|
"language_level": 3,
|
||||||
|
"embedsignature": True
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
_sdist.run(self)
|
||||||
|
|
||||||
|
|
||||||
|
# don't build extensions if user explicitly requested --without-cython
|
||||||
|
if with_cython is False:
|
||||||
|
extensions = []
|
||||||
|
else:
|
||||||
|
extensions = [
|
||||||
|
Extension("cu2qu.cu2qu", ["Lib/cu2qu/cu2qu.py"]),
|
||||||
|
]
|
||||||
|
|
||||||
with open('README.rst', 'r') as f:
|
with open('README.rst', 'r') as f:
|
||||||
long_description = f.read()
|
long_description = f.read()
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='cu2qu',
|
name='cu2qu',
|
||||||
version="1.5.1.dev0",
|
use_scm_version={"write_to": "Lib/cu2qu/_version.py"},
|
||||||
description='Cubic-to-quadratic bezier curve conversion',
|
description='Cubic-to-quadratic bezier curve conversion',
|
||||||
author="James Godfrey-Kittle, Behdad Esfahbod",
|
author="James Godfrey-Kittle, Behdad Esfahbod",
|
||||||
author_email="jamesgk@google.com",
|
author_email="jamesgk@google.com",
|
||||||
@ -174,8 +206,9 @@ setup(
|
|||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
packages=find_packages('Lib'),
|
packages=find_packages('Lib'),
|
||||||
package_dir={'': 'Lib'},
|
package_dir={'': 'Lib'},
|
||||||
|
ext_modules=extensions,
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
setup_requires=pytest_runner + wheel + bump2version,
|
setup_requires=pytest_runner + wheel + ["setuptools_scm"],
|
||||||
tests_require=[
|
tests_require=[
|
||||||
'pytest>=2.8',
|
'pytest>=2.8',
|
||||||
],
|
],
|
||||||
@ -185,10 +218,6 @@ setup(
|
|||||||
],
|
],
|
||||||
extras_require={"cli": ["defcon>=0.4.0"]},
|
extras_require={"cli": ["defcon>=0.4.0"]},
|
||||||
entry_points={"console_scripts": ["cu2qu = cu2qu.cli:main [cli]"]},
|
entry_points={"console_scripts": ["cu2qu = cu2qu.cli:main [cli]"]},
|
||||||
cmdclass={
|
|
||||||
"release": release,
|
|
||||||
"bump_version": bump_version,
|
|
||||||
},
|
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 4 - Beta',
|
'Development Status :: 4 - Beta',
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
@ -202,4 +231,5 @@ setup(
|
|||||||
'Topic :: Multimedia :: Graphics :: Editors :: Vector-Based',
|
'Topic :: Multimedia :: Graphics :: Editors :: Vector-Based',
|
||||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||||
],
|
],
|
||||||
|
cmdclass={"build_ext": cython_build_ext, "sdist": cython_sdist},
|
||||||
)
|
)
|
||||||
|
32
tools/update_cython_shadow.py
Normal file
32
tools/update_cython_shadow.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
""" Update the embedded Lib/cu2qu/cython.py module with the contents of
|
||||||
|
the latest cython repository.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
$ python tools/update_cython_shadow.py 0.28.5
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
header = b'''\
|
||||||
|
""" This module is copied verbatim from the "Cython.Shadow" module:
|
||||||
|
https://github.com/cython/cython/blob/master/Cython/Shadow.py
|
||||||
|
|
||||||
|
Cython is licensed under the Apache 2.0 Software License.
|
||||||
|
"""
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
version = sys.argv[1]
|
||||||
|
except IndexError:
|
||||||
|
version = "master"
|
||||||
|
|
||||||
|
CYTHON_SHADOW_URL = (
|
||||||
|
"https://raw.githubusercontent.com/cython/cython/%s/Cython/Shadow.py"
|
||||||
|
) % version
|
||||||
|
|
||||||
|
r = requests.get(CYTHON_SHADOW_URL, allow_redirects=True)
|
||||||
|
with open("Lib/cu2qu/cython.py", "wb") as f:
|
||||||
|
f.write(header)
|
||||||
|
f.write(r.content)
|
73
tox.ini
73
tox.ini
@ -1,18 +1,39 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist = py27, py36, htmlcov
|
envlist = py{27,37}-{cy,nocy}, htmlcov
|
||||||
|
package_name = cu2qu
|
||||||
|
; we skip tox's own sdist generation as we need to pass different environment
|
||||||
|
; variables for testing buiding with and without cython
|
||||||
|
skipsdist = true
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
|
setenv =
|
||||||
|
nocy: CU2QU_WITH_CYTHON=0
|
||||||
|
cy: CU2QU_WITH_CYTHON=1
|
||||||
|
cy: CYTHON_TRACE=1
|
||||||
|
cy: CYTHON_ANNOTATE=1
|
||||||
deps =
|
deps =
|
||||||
-rtest-requirements.txt
|
-rtest-requirements.txt
|
||||||
-rrequirements.txt
|
-rrequirements.txt
|
||||||
|
cy: cython
|
||||||
|
changedir = {toxinidir}
|
||||||
commands =
|
commands =
|
||||||
|
# create source distribution in a temp dir
|
||||||
|
python setup.py --quiet sdist --dist-dir {envtmpdir}
|
||||||
|
|
||||||
|
# install from sdist
|
||||||
|
python -m pip install --ignore-installed --pre --no-deps --no-cache-dir --find-links {envtmpdir} {[tox]package_name}
|
||||||
|
|
||||||
|
# ensure we are running the requested cu2qu version (compiled vs interpreted)
|
||||||
|
nocy: python -c "import sys, cu2qu.cu2qu; cu2qu.cu2qu.COMPILED and sys.exit(1)"
|
||||||
|
cy: python -c "import sys, cu2qu.cu2qu; cu2qu.cu2qu.COMPILED or sys.exit(1)"
|
||||||
|
|
||||||
|
# run tests with code coverage enabled
|
||||||
coverage run --parallel-mode -m pytest {posargs}
|
coverage run --parallel-mode -m pytest {posargs}
|
||||||
|
|
||||||
[testenv:htmlcov]
|
[testenv:htmlcov]
|
||||||
basepython = python3.6
|
|
||||||
deps =
|
deps =
|
||||||
coverage
|
coverage
|
||||||
skip_install = true
|
changedir = {toxinidir}
|
||||||
commands =
|
commands =
|
||||||
coverage combine
|
coverage combine
|
||||||
coverage report
|
coverage report
|
||||||
@ -23,8 +44,52 @@ passenv = *
|
|||||||
deps =
|
deps =
|
||||||
coverage
|
coverage
|
||||||
codecov
|
codecov
|
||||||
skip_install = true
|
|
||||||
ignore_outcome = true
|
ignore_outcome = true
|
||||||
|
changedir = {toxinidir}
|
||||||
commands =
|
commands =
|
||||||
coverage combine
|
coverage combine
|
||||||
codecov --env TRAVIS_PYTHON_VERSION
|
codecov --env TRAVIS_PYTHON_VERSION
|
||||||
|
|
||||||
|
[testenv:update-cython]
|
||||||
|
deps = requests
|
||||||
|
changedir = {toxinidir}
|
||||||
|
commands =
|
||||||
|
python tools/update_cython_shadow.py {posargs}
|
||||||
|
|
||||||
|
[testenv:sdist]
|
||||||
|
deps =
|
||||||
|
setuptools
|
||||||
|
cython
|
||||||
|
changedir = {toxinidir}
|
||||||
|
commands =
|
||||||
|
python -c 'import shutil; shutil.rmtree("dist", ignore_errors=True)'
|
||||||
|
python setup.py --with-cython sdist --dist-dir dist
|
||||||
|
|
||||||
|
[testenv:pure-wheel]
|
||||||
|
deps =
|
||||||
|
{[testenv:sdist]deps}
|
||||||
|
pip
|
||||||
|
wheel
|
||||||
|
setenv = CU2QU_WITH_CYTHON=0
|
||||||
|
changedir = {toxinidir}
|
||||||
|
commands =
|
||||||
|
{[testenv:sdist]commands}
|
||||||
|
pip wheel --pre --no-deps --no-cache-dir --wheel-dir dist --find-links dist \
|
||||||
|
--no-binary {[tox]package_name} {[tox]package_name}
|
||||||
|
|
||||||
|
[testenv:native-wheel]
|
||||||
|
deps = {[testenv:pure-wheel]deps}
|
||||||
|
setenv = CU2QU_WITH_CYTHON=1
|
||||||
|
changedir = {toxinidir}
|
||||||
|
commands = {[testenv:pure-wheel]commands}
|
||||||
|
|
||||||
|
; we only upload the pure cu2qu-*-py2.py3-any.whl (for now)
|
||||||
|
[testenv:pypi]
|
||||||
|
deps =
|
||||||
|
{[testenv:pure-wheel]deps}
|
||||||
|
twine
|
||||||
|
passenv = TWINE_USERNAME TWINE_PASSWORD
|
||||||
|
changedir = {toxinidir}
|
||||||
|
commands =
|
||||||
|
{[testenv:pure-wheel]commands}
|
||||||
|
twine upload dist/*.whl dist/*.zip
|
||||||
|
Loading…
x
Reference in New Issue
Block a user