diff --git a/.github/workflows/build_deploy.yml b/.github/workflows/build_deploy.yml new file mode 100644 index 0000000..ce64bb8 --- /dev/null +++ b/.github/workflows/build_deploy.yml @@ -0,0 +1,122 @@ +--- +name: Build and upload + +on: # yamllint disable-line rule:truthy + push: + paths-ignore: + - 'README.md' + release: + types: [created] + +jobs: + build_wheels: + name: Build wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + + if: github.event_name == 'release' && github.event.action == 'created' + steps: + - uses: actions/checkout@v4 + + - name: Build wheels + uses: pypa/cibuildwheel@v2.21.3 + env: + CIBW_TEST_COMMAND_WINDOWS: > + cd /d {package} + && ( rmdir ..\uwcwidth_tmp /s /q 2>NUL || cd . ) + && mkdir ..\uwcwidth_tmp + && cd ..\uwcwidth_tmp + && xcopy {package} /s + && rmdir uwcwidth /s /q + && pytest + + - uses: actions/upload-artifact@v4 + with: + name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} + path: ./wheelhouse/*.whl + + build_sdist: + name: Build source distribution + runs-on: ubuntu-latest + if: github.event_name == 'release' && github.event.action == 'created' + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: python3 -m pip install --upgrade build + + - name: Build sdist + run: python3 -m build --sdist + + - uses: actions/upload-artifact@v4 + with: + name: cibw-sdist + path: dist/*.tar.gz + + upload_pypi: + needs: [build_wheels, build_sdist] + runs-on: ubuntu-latest + environment: production + permissions: + id-token: write + if: | + github.event_name == 'release' && github.event.action == 'created' + && !endsWith(github.ref, '-test') + steps: + - uses: actions/download-artifact@v4 + with: + # unpacks all CIBW artifacts into dist/ + pattern: cibw-* + path: dist + merge-multiple: true + + - uses: pypa/gh-action-pypi-publish@release/v1 + + upload_pypi_test: + needs: [build_wheels, build_sdist] + runs-on: ubuntu-latest + environment: production + permissions: + id-token: write + if: | + github.event_name == 'release' && github.event.action == 'created' + && endsWith(github.ref, '-test') + steps: + - uses: actions/download-artifact@v4 + with: + # unpacks all CIBW artifacts into dist/ + pattern: cibw-* + path: dist + merge-multiple: true + + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + + upload_gh_release: + needs: [build_wheels, build_sdist] + runs-on: ubuntu-latest + environment: production + permissions: + contents: write + id-token: write + + steps: + - uses: actions/download-artifact@v4 + with: + # unpacks all CIBW artifacts into dist/ + pattern: cibw-* + path: dist + merge-multiple: true + + - uses: sigstore/gh-action-sigstore-python@v2.1.1 + with: + inputs: >- + ./dist/*.tar.gz + ./dist/*.whl + + - uses: softprops/action-gh-release@v2 + with: + files: dist/** diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..1387a2c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,17 @@ +name: Test + +on: + push: + paths-ignore: + - 'README.md' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + - run: make test diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d80ebc8 --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +.PHONY: all build build-debug clean clean-build clean-venv test + +all: build venv + @: + +build: venv + DEBUG=$(DEBUG) venv/bin/python3 setup.py build_ext --inplace + +build-debug: override DEBUG=1 +build-debug: build ; + +clean: clean-venv clean-build + @: + +clean-venv: + rm -rf venv + +clean-build: + rm -rf build + +test: build + venv/bin/pytest + +venv: + python3 -mvenv venv + venv/bin/pip install setuptools Cython pytest diff --git a/README.md b/README.md index f5ff67a..8fbe96d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,15 @@ -## Overview +## uwcwidth +*terminal width of Unicode 16.0+Emoji strings in nanoseconds* + +![PyPI - Python Version](https://img.shields.io/pypi/pyversions/uwcwidth) +[![PyPI - Version](https://img.shields.io/pypi/v/uwcwidth)](https://pypi.org/project/uwcwidth/) +[![PyPI - License](https://img.shields.io/pypi/l/uwcwidth)](https://github.com/Z4JC/uwcwidth/blob/main/LICENSE) +![PyPI - Downloads](https://img.shields.io/pypi/dm/uwcwidth)
+[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/Z4JC/uwcwidth/build_deploy.yml)](https://github.com/Z4JC/uwcwidth/actions/workflows/build_deploy.yml) +[![GitHub branch check runs](https://img.shields.io/github/check-runs/Z4JC/uwcwidth/main)](https://github.com/Z4JC/uwcwidth/actions/workflows/test.yml) +![PyPI - Status](https://img.shields.io/pypi/status/uwcwidth) +![PyPI - Wheel](https://img.shields.io/pypi/wheel/uwcwidth)
+ Use `uwcwidth` when you want to very quickly find out how many characters a Unicode string takes up in your terminal. For example, `uwcwidth.wcswidth('Hello🥹')` returns `7` because your terminal will use 5 places for "Hello" and then 2 places for the "🥹" emoji. @@ -40,20 +51,20 @@ See the `tests` folder for more. `uwcwidth` reserves around 4 KB of memory for its lookup tables. Parts of the storage scheme are derived from an older `wcwidth` implementation in [musl libc](https://musl.libc.org/). Generally sparse or dense bitmaps are used to look things up. The `uwcwidth.pyx` file is under 100 lines of code, with comments and whitespace. -## Performance: 30x faster than `wcwidth` -`uwcwidth` is about 30 times faster than the popular, well-documented and highly tested [wcwidth](https://github.com/jquast/wcwidth) library, while maintaining similar accuracy. It's also 5 times faster than `cwcwidth`, which does not work on new Emojis and breaks on some other edge cases. +## Performance: 40x faster than `wcwidth` +`uwcwidth` is about 40 times faster than the popular, well-documented and highly tested [wcwidth](https://github.com/jquast/wcwidth) library, while maintaining similar accuracy. It's also 5 times faster than `cwcwidth`, which does not work on new Emojis and breaks on some other edge cases. ```python3 In [1]: import wcwidth, cwcwidth, uwcwidth In [2]: %%timeit ...: wcwidth.wcswidth("コンニチハ, セカイ!") -1.28 μs ± 6.22 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each) +1.73 μs ± 7.93 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each) In [3]: %%timeit ...: cwcwidth.wcswidth("コンニチハ, セカイ!") -205 ns ± 0.408 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each) +211 ns ± 3.63 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each) In [4]: %%timeit ...: uwcwidth.wcswidth("コンニチハ, セカイ!") -38.5 ns ± 0.29 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each) -``` \ No newline at end of file +41 ns ± 0.0363 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each) +``` diff --git a/pyproject.toml b/pyproject.toml index 66ab3ef..8ad213d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uwcwidth" -version = "0.9.2" +version = "1.0.0" authors = [{name = "!ZAJC!"}] readme = "README.md" description = "terminal width of Unicode 16.0+Emoji strings in nanoseconds" @@ -33,5 +33,17 @@ exclude = [] namespaces = false [tool.pytest.ini_options] +pythonpath = ["."] testpaths = ["tests"] addopts = ["--import-mode=importlib"] + +[tool.cibuildwheel] +build-frontend = "build" +test-command = """ +cd $( mktemp -d ) \ +&& cp -pr {project}/* ./ \ +&& rm -rf uwcwidth \ +&& pytest +""" +test-requires = "pytest" +skip = ["cp36-*", "cp37-*"] diff --git a/setup.py b/setup.py index 1ce64d1..60d24c7 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,24 @@ # SPDX-License-Identifier: MIT +import os +import platform + from setuptools import setup, Extension + +DEBUG=(os.getenv('DEBUG') or '').strip().lower() in ['1', 'y', 'true'] +MSVC=(platform.platform().startswith('Windows') and + platform.python_compiler().startswith('MS')) +COMPILE_ARGS=[] if MSVC else (["-g", "-O0", "-UNDEBUG"] if DEBUG else ["-O3"]) + + +def uwcwidth_ext(module, pyx_file): + return Extension(module, + sources=[pyx_file], + extra_compile_args=COMPILE_ARGS) + + setup( name='uwcwidth', - ext_modules=[Extension("uwcwidth.uwcwidth", - sources=["uwcwidth/uwcwidth.pyx"])], + ext_modules=[uwcwidth_ext("uwcwidth.uwcwidth", "uwcwidth/uwcwidth.pyx")], package_data={'uwcwidth': ['__init__.pxd', 'uwcwidth.pxd', 'tables.pxd']} )