Merge pull request #153 from googlei18n/cython-setup

set up optional cython extension module
This commit is contained in:
Cosimo Lupo 2018-09-26 20:22:12 +01:00 committed by GitHub
commit 4ee6f7c15a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1162 additions and 544 deletions

View File

@ -6,6 +6,10 @@ branch = True
# list of directories or packages to measure
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
[paths]
source =

10
.gitignore vendored
View File

@ -1,7 +1,12 @@
# Byte-compiled and optimized files
__pycache__/
*.py[co]
*.py[cod]
*$py.class
*.so
# cython generated C/HTML files
Lib/cu2qu/*.c
Lib/cu2qu/*.html
# Packaging
*.egg-info
@ -19,3 +24,6 @@ htmlcov
# OS X Finder
.DS_Store
# auto-generated version file
Lib/cu2qu/_version.py

View File

@ -1,29 +1,37 @@
sudo: false
language: python
python:
- "2.7"
- "3.6"
env:
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:
only:
- master
- /^v\d+\.\d+.*$/
install: pip install tox-travis
install: pip install tox
script: tox
after_success: tox -e codecov
deploy:
after_success:
- if [ -z "$TRAVIS_TAG" ]; then tox -e codecov; fi
# deploy to PyPI on tags
provider: pypi
server: https://upload.pypi.org/legacy/
on:
repo: googlei18n/cu2qu
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
- |
if [ -n "$TRAVIS_TAG" ] && [ "$TRAVIS_REPO_SLUG" == "googlei18n/cu2qu" ] && [ "$BUILD_DIST" == true ]; then
tox -e pypi
fi

View File

@ -15,350 +15,9 @@
from __future__ import print_function, division, absolute_import
__version__ = "1.5.1.dev0"
__all__ = ['curve_to_quadratic', 'curves_to_quadratic']
MAX_N = 100
try:
import cython
except:
class Cython:
@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 ._version import version as __version__
except ImportError:
__version__ = "0.0.0+unknown"
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()
from .cu2qu import *

View File

@ -1,3 +1,4 @@
from __future__ import print_function, division, absolute_import
import os
import argparse
import logging

370
Lib/cu2qu/cu2qu.py Normal file
View 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
View 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

View File

@ -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]
universal = 1
@ -38,16 +12,15 @@ license_file = LICENSE
[tool:pytest]
minversion = 3.0
testpaths =
testpaths =
tests
python_files =
python_files =
*_test.py
python_classes =
python_classes =
*Test
addopts =
addopts =
-s
-v
-r a
--doctest-modules
--doctest-ignore-import-errors

320
setup.py
View File

@ -13,159 +13,191 @@
# limitations under the License.
from setuptools import setup, find_packages, Command
import sys
from setuptools import setup, find_packages, Extension
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
class bump_version(Command):
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)
import sys
import os
import re
from io import open
needs_pytest = {'pytest', 'test'}.intersection(sys.argv)
pytest_runner = ['pytest_runner'] if needs_pytest else []
needs_wheel = {'bdist_wheel'}.intersection(sys.argv)
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:
long_description = f.read()
setup(
name='cu2qu',
version="1.5.1.dev0",
use_scm_version={"write_to": "Lib/cu2qu/_version.py"},
description='Cubic-to-quadratic bezier curve conversion',
author="James Godfrey-Kittle, Behdad Esfahbod",
author_email="jamesgk@google.com",
@ -174,8 +206,9 @@ setup(
long_description=long_description,
packages=find_packages('Lib'),
package_dir={'': 'Lib'},
ext_modules=extensions,
include_package_data=True,
setup_requires=pytest_runner + wheel + bump2version,
setup_requires=pytest_runner + wheel + ["setuptools_scm"],
tests_require=[
'pytest>=2.8',
],
@ -185,10 +218,6 @@ setup(
],
extras_require={"cli": ["defcon>=0.4.0"]},
entry_points={"console_scripts": ["cu2qu = cu2qu.cli:main [cli]"]},
cmdclass={
"release": release,
"bump_version": bump_version,
},
classifiers=[
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
@ -202,4 +231,5 @@ setup(
'Topic :: Multimedia :: Graphics :: Editors :: Vector-Based',
'Topic :: Software Development :: Libraries :: Python Modules',
],
cmdclass={"build_ext": cython_build_ext, "sdist": cython_sdist},
)

View 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
View File

@ -1,18 +1,39 @@
[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]
setenv =
nocy: CU2QU_WITH_CYTHON=0
cy: CU2QU_WITH_CYTHON=1
cy: CYTHON_TRACE=1
cy: CYTHON_ANNOTATE=1
deps =
-rtest-requirements.txt
-rrequirements.txt
cy: cython
changedir = {toxinidir}
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}
[testenv:htmlcov]
basepython = python3.6
deps =
coverage
skip_install = true
changedir = {toxinidir}
commands =
coverage combine
coverage report
@ -23,8 +44,52 @@ passenv = *
deps =
coverage
codecov
skip_install = true
ignore_outcome = true
changedir = {toxinidir}
commands =
coverage combine
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