From 13c3fb5782fea460bdd4454843c94e0d5dc9c00f Mon Sep 17 00:00:00 2001 From: Zhang Yanpo Date: Wed, 7 Jan 2026 23:28:32 +0800 Subject: [PATCH 1/2] refactor: migrate from setup.py to pyproject.toml Modernize build configuration to PEP 621 compliant pyproject.toml and update tooling to use ruff for linting/formatting and MkDocs for documentation. Changes: - Replace `setup.py` with `pyproject.toml` - Update `__init__.py` to use `importlib.metadata` for version - Switch from Sphinx to MkDocs with mkdocstrings - Replace flake8 with ruff - Update GitHub Actions to v5 - Remove obsolete _building scripts --- .github/workflows/python-package.yml | 86 ++---------- .github/workflows/python-publish.yml | 44 +++---- .gitignore | 1 + .readthedocs.yaml | 16 +++ __init__.py | 9 +- _building/.gitignore | 129 ------------------ _building/Makefile | 3 - _building/README.md | 35 ++--- _building/README.md.j2 | 38 ------ _building/__init__.py | 129 ------------------ _building/build_readme.py | 85 ------------ _building/build_setup.py | 190 --------------------------- _building/building-requirements.txt | 7 - _building/common.mk | 23 ++-- _building/install.sh | 12 -- _building/populate.py | 23 ++-- _building/requirements.txt | 1 - docs/Makefile | 20 --- docs/index.md | 37 ++++++ docs/make.bat | 35 ----- docs/source/conf.py | 28 ---- docs/source/index.rst | 26 ---- mkdocs.yml | 30 +++++ pyproject.toml | 57 ++++++++ requirements.txt | 4 - setup.py | 23 ---- test/test_k3utfjson.py | 87 ++++++------ utfjson.py | 6 +- 28 files changed, 271 insertions(+), 913 deletions(-) create mode 100644 .readthedocs.yaml delete mode 100644 _building/.gitignore delete mode 100644 _building/Makefile delete mode 100644 _building/README.md.j2 delete mode 100644 _building/__init__.py delete mode 100644 _building/build_readme.py delete mode 100644 _building/build_setup.py delete mode 100644 _building/building-requirements.txt delete mode 100755 _building/install.sh delete mode 100644 _building/requirements.txt delete mode 100644 docs/Makefile create mode 100644 docs/index.md delete mode 100644 docs/make.bat delete mode 100644 docs/source/conf.py delete mode 100644 docs/source/index.rst create mode 100644 mkdocs.yml create mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100644 setup.py diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 57ad3e7..4575dad 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -1,6 +1,3 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - name: Unit test on: @@ -9,116 +6,61 @@ on: jobs: ut: - runs-on: ${{ matrix.os }} strategy: matrix: - # Github action runner update ubuntu to 22.04, which does not have - # python-3.6 in it: https://github.com/actions/setup-python/issues/544#issuecomment-1320295576 - # To fix it: use os: [ubuntu-20.04] instead os: [ubuntu-latest] python-version: [3.9, "3.10", 3.11, 3.12] steps: - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - if [ -f test-requirements.txt ]; then pip install -r test-requirements.txt; fi - - name: Install npm dependencies - run: | - if [ -f package.json ]; then npm install; fi - - # manually add module binary path to github ci - echo "add node module path: $GITHUB_WORKSPACE/node_modules/.bin/" - echo "$GITHUB_WORKSPACE/node_modules/.bin/" >> $GITHUB_PATH - - - name: Install apt dependencies - run: | - if [ -f packages.txt ]; then cat packages.txt | xargs sudo apt-get install; fi + pip install -e . - name: Test with pytest - env: - # interactive command such as k3handy.cmdtty to run git, git complains - # if no TERM set: - # out: - (press RETURN) - # err: WARNING: terminal is not fully functional - # And waiting for a RETURN to press for ever - TERM: xterm run: | - cp setup.py .. - cd .. - python setup.py install - cd - - - if [ -f sudo_test ]; then - sudo env "PATH=$PATH" pytest -v - else - pytest -v - fi - - - uses: actions/upload-artifact@v4 - if: failure() - with: - path: test/ + pytest -v build_doc: - runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest] - python-version: [3.9, "3.10", 3.11, 3.12] + python-version: ["3.12"] steps: - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - if [ -f test-requirements.txt ]; then pip install -r test-requirements.txt; fi - - name: Test building doc run: | - pip install -r _building/building-requirements.txt - make -C docs html + pip install -e . + pip install mkdocs mkdocs-material "mkdocstrings[python]" + mkdocs build lint: - runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest] - python-version: [3.9, "3.10", 3.11, 3.12] + python-version: ["3.12"] steps: - uses: actions/checkout@v5 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install flake8 - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - if [ -f test-requirements.txt ]; then pip install -r test-requirements.txt; fi - - - name: Lint with flake8 + - name: Lint with ruff run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + pip install ruff + ruff check . + ruff format --check . diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 3fd68f1..af931b2 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -1,6 +1,3 @@ -# This workflows will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries - name: Upload Python Package on: @@ -10,27 +7,26 @@ on: jobs: deploy: - runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - cp setup.py .. - cd .. - python setup.py sdist bdist_wheel - pip install dist/*.tar.gz - python -c 'import '${GITHUB_REPOSITORY#*/} - twine upload dist/* + - uses: actions/checkout@v5 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build package + run: python -m build + + - name: Publish to PyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: twine upload dist/* diff --git a/.gitignore b/.gitignore index 0569721..94d8aab 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,4 @@ dmypy.json .pyre/ .claude/ +site/ diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..3e517df --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,16 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +mkdocs: + configuration: mkdocs.yml + +python: + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/__init__.py b/__init__.py index 31ff27f..bb552cc 100644 --- a/__init__.py +++ b/__init__.py @@ -12,8 +12,9 @@ # from .proc import CalledProcessError # from .proc import ProcError -__version__ = "0.1.1" -__name__ = "k3utfjson" +from importlib.metadata import version + +__version__ = version("k3utfjson") from .utfjson import ( dump, @@ -21,6 +22,6 @@ ) __all__ = [ - 'dump', - 'load', + "dump", + "load", ] diff --git a/_building/.gitignore b/_building/.gitignore deleted file mode 100644 index b6e4761..0000000 --- a/_building/.gitignore +++ /dev/null @@ -1,129 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ diff --git a/_building/Makefile b/_building/Makefile deleted file mode 100644 index e5b5931..0000000 --- a/_building/Makefile +++ /dev/null @@ -1,3 +0,0 @@ -# make readme name -readme: - cd .. && python _building/build_readme.py diff --git a/_building/README.md b/_building/README.md index 50205e1..0ee458e 100644 --- a/_building/README.md +++ b/_building/README.md @@ -1,23 +1,24 @@ -# building -building toolkit for pykit3 repos +# _building -This repo should be included in a package, e.g.: +Shared build configuration for pykit3 packages. -``` -vcs/pykit3/k3handy/ -▸ .git/ -▸ .github/ -▸ __pycache__/ -▾ _building/ <-- this repo - ... -``` +## Commands -# Publish python package: +All commands use the `pk3` package: -- `make build_setup_py` does the following steps: - - Builds the `setup.py` and commit it. - - Add a git tag with the name of `"v" + __init__.__ver__`. +```bash +make test # Run tests with pytest +make lint # Format and lint with ruff +make cov # Generate coverage report +make doc # Build documentation with mkdocs +make readme # Generate README.md from docstrings +make release # Create git tag from version in pyproject.toml +make publish # Build and upload to PyPI +``` -- Then `git push` the tag, github Action in the `.github/workflows/python-pubish.yml` will publish a package to `pypi`. +## Release Process - The action spec is copied from template repo: `github.com/pykit3/tmpl`. +1. Update version in `pyproject.toml` +2. Run `make release` to create git tag +3. Run `git push --tags` to trigger GitHub Actions +4. GitHub Actions automatically publishes to PyPI diff --git a/_building/README.md.j2 b/_building/README.md.j2 deleted file mode 100644 index d9baf2c..0000000 --- a/_building/README.md.j2 +++ /dev/null @@ -1,38 +0,0 @@ -# {{ name }} - -[![Action-CI](https://github.com/pykit3/{{ name }}/actions/workflows/python-package.yml/badge.svg)](https://github.com/pykit3/{{ name }}/actions/workflows/python-package.yml) -[![Build Status](https://travis-ci.com/pykit3/{{ name }}.svg?branch=master)](https://travis-ci.com/pykit3/{{ name }}) -[![Documentation Status](https://readthedocs.org/projects/{{ name }}/badge/?version=stable)](https://{{ name }}.readthedocs.io/en/stable/?badge=stable) -[![Package](https://img.shields.io/pypi/pyversions/{{ name }})](https://pypi.org/project/{{ name }}) - -{{ description }} - -{{ name }} is a component of [pykit3] project: a python3 toolkit set. - -{{ package_doc }} - - -# Install - -``` -pip install {{ name }} -``` - -# Synopsis - -```python -{{ synopsis }} -``` - -# Author - -Zhang Yanpo (张炎泼) - -# Copyright and License - -The MIT License (MIT) - -Copyright (c) 2015 Zhang Yanpo (张炎泼) - - -[pykit3]: https://github.com/pykit3 diff --git a/_building/__init__.py b/_building/__init__.py deleted file mode 100644 index 1055c67..0000000 --- a/_building/__init__.py +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 - -import importlib.util -import sys - -# sys.path.insert(0, os.path.abspath('..')) -# sys.path.insert(0, os.path.abspath('../..')) -# sys.path.insert(0, os.path.abspath('../../..')) - -# __title__ = 'requests' -# __description__ = 'Python HTTP for Humans.' -# __url__ = 'https://requests.readthedocs.io' -# __version__ = '2.23.0' - -__author__ = "Zhang Yanpo" -__author_email__ = "drdr.xp@gmail.com" -__license__ = "MIT" -__copyright__ = "Copyright 2020 Zhang Yanpo" - - -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.napoleon", - # "sphinx.ext.intersphinx", - # "sphinx.ext.todo", - # "sphinx.ext.viewcode", -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ["_templates"] - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = [] - - -master_doc = "index" - - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = "alabaster" - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -# html_static_path = ['_static'] -html_static_path = [] - - -def load_parent_package(): - """ - Load the parent directory as a package module. - - Returns: - tuple: (package_name, package_module) - """ - import os - - parent_dir = os.path.dirname(os.path.dirname(__file__)) - if parent_dir not in sys.path: - sys.path.insert(0, parent_dir) - - # Read the __init__.py file to get the package name - init_file = os.path.join(parent_dir, "__init__.py") - package_name = None - - with open(init_file, "r") as f: - for line in f: - if line.strip().startswith("__name__"): - # Extract package name from __name__ = "package_name" - package_name = line.split("=")[1].strip().strip("\"'") - break - - if not package_name: - # Fallback: use directory name - package_name = os.path.basename(parent_dir) - - # Load the module with proper package context using importlib - spec = importlib.util.spec_from_file_location(package_name, init_file) - pkg = importlib.util.module_from_spec(spec) - sys.modules[package_name] = pkg # Add to sys.modules so relative imports work - spec.loader.exec_module(pkg) - - return package_name, pkg - - -def sphinx_confs(): - """ - Load repo dir as a package - - `readthedocs` use branch name as dir! - Thus the following does not work:: - - import pk3proc - """ - - print("sys.path:", sys.path) - - package_name, pkg = load_parent_package() - - return ( - pkg.__name__, - pkg, - pkg.__version__, - __author__, - __copyright__, - extensions, - templates_path, - exclude_patterns, - master_doc, - html_theme, - html_static_path, - ) diff --git a/_building/build_readme.py b/_building/build_readme.py deleted file mode 100644 index d211e29..0000000 --- a/_building/build_readme.py +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 - -import doctest -import os -import sys - -import jinja2 -import yaml - -from __init__ import load_parent_package - -# xxx/_building/build_readme.py -this_base = os.path.dirname(__file__) - -j2vars = {} - -# let it be able to find indirectly dependent package locally -# e.g.: `k3fs` depends on `k3confloader` -sys.path.insert(0, os.path.abspath("..")) - -# load package name from __init__.py -package_name, pkg = load_parent_package() -j2vars["name"] = package_name - - -def get_gh_config(): - with open(".github/settings.yml", "r") as f: - cont = f.read() - - cfg = yaml.safe_load(cont) - tags = cfg["repository"]["topics"].split(",") - tags = [x.strip() for x in tags] - cfg["repository"]["topics"] = tags - return cfg - - -cfg = get_gh_config() -j2vars["description"] = cfg["repository"]["description"] - - -def get_examples(pkg): - doc = pkg.__doc__ - parser = doctest.DocTestParser() - es = parser.get_examples(doc) - rst = [] - for e in es: - rst.append(">>> " + e.source.strip()) - rst.append(e.want.strip()) - - rst = "\n".join(rst) - - for fn in ( - "synopsis.txt", - "synopsis.py", - ): - try: - with open(fn, "r") as f: - rst += "\n" + f.read() - - except FileNotFoundError: - pass - - return rst - - -j2vars["synopsis"] = get_examples(pkg) -j2vars["package_doc"] = pkg.__doc__ - - -def render_j2(tmpl_path, tmpl_vars, output_path): - template_loader = jinja2.FileSystemLoader(searchpath="./") - template_env = jinja2.Environment( - loader=template_loader, undefined=jinja2.StrictUndefined - ) - template = template_env.get_template(tmpl_path) - - txt = template.render(tmpl_vars) - - with open(output_path, "w") as f: - f.write(txt) - - -if __name__ == "__main__": - render_j2("_building/README.md.j2", j2vars, "README.md") diff --git a/_building/build_setup.py b/_building/build_setup.py deleted file mode 100644 index ca2ec6c..0000000 --- a/_building/build_setup.py +++ /dev/null @@ -1,190 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 - -""" -build steup.py for this package. -""" - -import ast -import subprocess -import sys -from string import Template - -import requirements -import yaml - -if hasattr(sys, "getfilesystemencoding"): - defenc = sys.getfilesystemencoding() -if defenc is None: - defenc = sys.getdefaultencoding() - - -def parse_assignment(filename, var_name): - """Parse a Python file and extract the value of a variable assignment using AST.""" - with open(filename, "r") as f: - content = f.read() - - tree = ast.parse(content) - - for node in ast.walk(tree): - if isinstance(node, ast.Assign): - # Check if this assignment is to our target variable - for target in node.targets: - if isinstance(target, ast.Name) and target.id == var_name: - # Extract the literal value - if isinstance(node.value, ast.Constant): # Python 3.8+ - return node.value.value - elif isinstance(node.value, ast.Str): # Python < 3.8 - return node.value.s - elif isinstance(node.value, ast.Num): # Python < 3.8 - return node.value.n - - return None - - -def get_name(): - name = parse_assignment("__init__.py", "__name__") - return name if name is not None else "k3git" # fallback - - -name = get_name() - - -def get_ver(): - version = parse_assignment("__init__.py", "__version__") - if version is None: - raise ValueError("Could not find __version__ in __init__.py") - return version - - -def get_gh_config(): - with open(".github/settings.yml", "r") as f: - cont = f.read() - - cfg = yaml.safe_load(cont) - tags = cfg["repository"]["topics"].split(",") - tags = [x.strip() for x in tags] - cfg["repository"]["topics"] = tags - return cfg - - -def get_travis(): - try: - with open(".travis.yml", "r") as f: - cont = f.read() - except OSError: - return None - - cfg = yaml.safe_load(cont) - return cfg - - -def get_compatible(): - # https://pypi.org/classifiers/ - - rst = [] - t = get_travis() - if t is None: - return ["Programming Language :: Python :: 3"] - - for v in t["python"]: - if v.startswith("pypy"): - v = "Implementation :: PyPy" - rst.append("Programming Language :: Python :: {}".format(v)) - - return rst - - -def get_req(): - try: - with open("requirements.txt", "r") as f: - req = list(requirements.parse(f)) - except OSError: - req = [] - - # req.name, req.specs, req.extras - # Django [('>=', '1.11'), ('<', '1.12')] - # six [('==', '1.10.0')] - req = [x.name + ",".join([a + b for a, b in x.specs]) for x in req] - - return req - - -cfg = get_gh_config() - -ver = get_ver() -description = cfg["repository"]["description"] -long_description = open("README.md").read() -req = get_req() -prog = get_compatible() - - -tmpl = """# DO NOT EDIT!!! built with `python _building/build_setup.py` -import setuptools -setuptools.setup( - name="${name}", - packages=["${name}"], - version="$ver", - license='MIT', - description=$description, - long_description=$long_description, - long_description_content_type="text/markdown", - author='Zhang Yanpo', - author_email='drdr.xp@gmail.com', - url='https://github.com/pykit3/$name', - keywords=$topics, - python_requires='>=3.0', - - install_requires=$req, - classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'Topic :: Software Development :: Libraries', - ] + $prog, -) -""" - -s = Template(tmpl) -rst = s.substitute( - name=name, - ver=ver, - description=repr(description), - long_description=repr(long_description), - topics=repr(cfg["repository"]["topics"]), - req=repr(req), - prog=repr(prog), -) -with open("setup.py", "w") as f: - f.write(rst) - - -sb = subprocess.Popen( - ["git", "add", "setup.py"], - encoding=defenc, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, -) -out, err = sb.communicate() -if sb.returncode != 0: - raise Exception("failure to add: ", out, err) - -sb = subprocess.Popen( - ["git", "commit", "setup.py", "-m", "release: v" + ver], - encoding=defenc, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, -) -out, err = sb.communicate() -if sb.returncode != 0: - raise Exception("failure to commit new release: " + ver, out, err) - - -sb = subprocess.Popen( - ["git", "tag", "v" + ver], - encoding=defenc, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, -) -out, err = sb.communicate() -if sb.returncode != 0: - raise Exception("failure to add tag: " + ver, out, err) diff --git a/_building/building-requirements.txt b/_building/building-requirements.txt deleted file mode 100644 index bd303df..0000000 --- a/_building/building-requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -# requirements for building doc, setup.py etc - -semantic_version>=2.9,<3.0 -jinja2>=3.0,<4.0 -PyYAML>=6.0,<7.0 - -sphinx>=4.3,<6.0 diff --git a/_building/common.mk b/_building/common.mk index dbac102..571715a 100644 --- a/_building/common.mk +++ b/_building/common.mk @@ -1,14 +1,19 @@ all: test lint readme doc -.PHONY: test lint +.PHONY: test lint cov sudo_test: - sudo env "PATH=$$PATH" UT_DEBUG=0 PYTHONPATH="$$(cd ..; pwd)" python -m unittest discover -c --failfast -s . + sudo env "PATH=$$PATH" UT_DEBUG=0 pytest -v test: - env "PATH=$$PATH" UT_DEBUG=0 PYTHONPATH="$$(cd ..; pwd)" python -m unittest discover -c --failfast -s . + env UT_DEBUG=0 pytest -v + +cov: + coverage run --source=. -m pytest + coverage html + open htmlcov/index.html doc: - make -C docs html + mkdocs build lint: # ruff format: fast Python code formatter (Black-compatible) @@ -21,13 +26,13 @@ static_check: uvx mypy . --ignore-missing-imports readme: - python _building/build_readme.py + pk3 readme -build_setup_py: - PYTHONPATH="$$(cd ..; pwd)" python _building/build_setup.py +release: + pk3 tag publish: - ./_building/publish.sh + pk3 publish install: - ./_building/install.sh + pip install -e . diff --git a/_building/install.sh b/_building/install.sh deleted file mode 100755 index e82727c..0000000 --- a/_building/install.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh - - -pwd="$(pwd)" -name="${pwd##*/}" -pip uninstall -y $name - -cp setup.py .. -( -cd .. -python setup.py install -) diff --git a/_building/populate.py b/_building/populate.py index 77deb12..d0a8731 100755 --- a/_building/populate.py +++ b/_building/populate.py @@ -15,22 +15,22 @@ def pjoin(*args): def cp(fn): - - cur = os.path.abspath('.') + cur = os.path.abspath(".") name = os.path.split(cur)[1] - t = '_building/tmpl/' - relfn = fn[len(t):] + t = "_building/tmpl/" + relfn = fn[len(t) :] base = os.path.split(fn)[0] if not os.path.exists(base): os.makedirs(base) src = fn - dst = re.sub(r'xxnamexx', name, relfn) + dst = re.sub(r"xxnamexx", name, relfn) - vs = {'name': name, - 'nameBig': name[0].upper() + name[1:], - } + vs = { + "name": name, + "nameBig": name[0].upper() + name[1:], + } print("populate ", src, " to ", dst) render_j2(src, vs, dst) @@ -39,14 +39,13 @@ def cp(fn): def render_j2(tmpl_path, tmpl_vars, output_path): - template_loader = jinja2.FileSystemLoader(searchpath='./') - template_env = jinja2.Environment(loader=template_loader, - undefined=jinja2.StrictUndefined) + template_loader = jinja2.FileSystemLoader(searchpath="./") + template_env = jinja2.Environment(loader=template_loader, undefined=jinja2.StrictUndefined) template = template_env.get_template(tmpl_path) txt = template.render(tmpl_vars) - with open(output_path, 'w') as f: + with open(output_path, "w") as f: f.write(txt) diff --git a/_building/requirements.txt b/_building/requirements.txt deleted file mode 100644 index 49fe098..0000000 --- a/_building/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -setuptools diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index a4de0bf..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -W -SPHINXBUILD ?= sphinx-build -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..6a77b59 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,37 @@ +# k3utfjson + +[![Action-CI](https://github.com/pykit3/k3utfjson/actions/workflows/python-package.yml/badge.svg)](https://github.com/pykit3/k3utfjson/actions/workflows/python-package.yml) +[![Documentation Status](https://readthedocs.org/projects/k3utfjson/badge/?version=stable)](https://k3utfjson.readthedocs.io/en/stable/?badge=stable) +[![Package](https://img.shields.io/pypi/pyversions/k3utfjson)](https://pypi.org/project/k3utfjson) + +JSON dump and load with enforced UTF-8 encoding. + +k3utfjson is a component of [pykit3](https://github.com/pykit3) project: a python3 toolkit set. + +## Installation + +```bash +pip install k3utfjson +``` + +## Quick Start + +```python +import k3utfjson + +# Load JSON from string (UTF-8) +data = k3utfjson.load('"hello 世界"') +print(data) # hello 世界 + +# Dump to JSON string (UTF-8) +json_str = k3utfjson.dump({'msg': '你好'}) +print(json_str) # {"msg": "你好"} +``` + +## API Reference + +::: k3utfjson + +## License + +The MIT License (MIT) - Copyright (c) 2015 Zhang Yanpo (张炎泼) diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 6247f7e..0000000 --- a/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index e436b3e..0000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,28 +0,0 @@ -import os -import sys - -sys.path.insert(0, os.path.abspath("../..")) - -# In order to find indirect dependency -sys.path.insert(0, os.path.abspath("../../..")) - -# use a try to force not to reorder sys.path and import. -try: - import _building -except Exception as e: - raise e - - -( - project, - pkg, - release, - author, - copyright, - extensions, - templates_path, - exclude_patterns, - master_doc, - html_theme, - html_static_path, -) = _building.sphinx_confs() diff --git a/docs/source/index.rst b/docs/source/index.rst deleted file mode 100644 index b81eed0..0000000 --- a/docs/source/index.rst +++ /dev/null @@ -1,26 +0,0 @@ -.. k3utfjson documentation master file, created by - sphinx-quickstart on Thu May 14 16:58:55 2020. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -k3utfjson -============ - -.. automodule:: k3utfjson - -Documentation for the Code -************************** - -Functions ---------- - -.. autofunction:: dump - -.. autofunction:: load - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..2ec9079 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,30 @@ +site_name: k3utfjson +site_description: DESCRIPTION +site_url: https://k3utfjson.readthedocs.io +repo_url: https://github.com/pykit3/k3utfjson +repo_name: pykit3/k3utfjson + +theme: + name: material + palette: + primary: blue + accent: blue + +plugins: + - search + - mkdocstrings: + handlers: + python: + paths: [.] + options: + show_source: true + show_root_heading: true + heading_level: 2 + +nav: + - Home: index.md + +markdown_extensions: + - admonition + - pymdownx.highlight + - pymdownx.superfences diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..922d6f1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,57 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "k3utfjson" +version = "0.1.2" +description = "JSON dump and load with enforced UTF-8 encoding" +readme = "README.md" +license = {text = "MIT"} +requires-python = ">=3.9" +authors = [ + { name = "Zhang Yanpo", email = "drdr.xp@gmail.com" } +] +keywords = ["json", "utf-8", "encoding", "serialization"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [] + +[project.urls] +Homepage = "https://github.com/pykit3/k3utfjson" +Documentation = "https://k3utfjson.readthedocs.io" + +[project.optional-dependencies] +dev = [ + "pytest>=7.0", + "ruff", + "coverage", +] +publish = [ + "build", + "twine", + "pk3", +] +docs = [ + "mkdocs>=1.5", + "mkdocs-material>=9.0", + "mkdocstrings[python]>=0.24", +] + +[tool.setuptools] +packages = ["k3utfjson"] + +[tool.setuptools.package-dir] +k3utfjson = "." + +[tool.ruff] +line-length = 120 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 3572afe..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ --r _building/requirements.txt - - -k3ut>=0.1.15,<0.2 diff --git a/setup.py b/setup.py deleted file mode 100644 index 064adee..0000000 --- a/setup.py +++ /dev/null @@ -1,23 +0,0 @@ -# DO NOT EDIT!!! built with `python _building/build_setup.py` -import setuptools -setuptools.setup( - name="k3utfjson", - packages=["k3utfjson"], - version="0.1.1", - license='MIT', - description='force `json.dump` and `json.load` in `utf-8` encoding.', - long_description='# k3utfjson\n\n[![Action-CI](https://github.com/pykit3/k3utfjson/actions/workflows/python-package.yml/badge.svg)](https://github.com/pykit3/k3utfjson/actions/workflows/python-package.yml)\n[![Build Status](https://travis-ci.com/pykit3/k3utfjson.svg?branch=master)](https://travis-ci.com/pykit3/k3utfjson)\n[![Documentation Status](https://readthedocs.org/projects/k3utfjson/badge/?version=stable)](https://k3utfjson.readthedocs.io/en/stable/?badge=stable)\n[![Package](https://img.shields.io/pypi/pyversions/k3utfjson)](https://pypi.org/project/k3utfjson)\n\nforce `json.dump` and `json.load` in `utf-8` encoding.\n\nk3utfjson is a component of [pykit3] project: a python3 toolkit set.\n\n\n# Name\n\nutfjson: force `json.dump` and `json.load` in `utf-8` encoding.\n\n# Status\n\nThis library is considered production ready.\n\n\n# Install\n\n```\npip install k3utfjson\n```\n\n# Synopsis\n\n```python\n\nimport k3utfjson\n\nk3utfjson.load(\'"hello"\')\nk3utfjson.dump({})\n\n```\n\n# Author\n\nZhang Yanpo (张炎泼) \n\n# Copyright and License\n\nThe MIT License (MIT)\n\nCopyright (c) 2015 Zhang Yanpo (张炎泼) \n\n\n[pykit3]: https://github.com/pykit3', - long_description_content_type="text/markdown", - author='Zhang Yanpo', - author_email='drdr.xp@gmail.com', - url='https://github.com/pykit3/k3utfjson', - keywords=['python', 'json'], - python_requires='>=3.0', - - install_requires=['k3ut<0.2,>=0.1.15'], - classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'Topic :: Software Development :: Libraries', - ] + ['Programming Language :: Python :: 3'], -) diff --git a/test/test_k3utfjson.py b/test/test_k3utfjson.py index 1de8a96..e7813e5 100644 --- a/test/test_k3utfjson.py +++ b/test/test_k3utfjson.py @@ -11,14 +11,13 @@ class TestUTFJson(unittest.TestCase): - def test_load(self): self.assertEqual(None, k3utfjson.load(None)) - self.assertEqual({}, k3utfjson.load('{}')) + self.assertEqual({}, k3utfjson.load("{}")) # load unicode, result in utf-8 - self.assertEqual('我', k3utfjson.load('"\\u6211"')) + self.assertEqual("我", k3utfjson.load('"\\u6211"')) self.assertEqual(str, type(k3utfjson.load('"\\u6211"'))) # unicode and string in a dictionary. @@ -33,50 +32,50 @@ def test_load(self): # load utf-8, result in str rst = k3utfjson.load(b'"\xe6\x88\x91"') - self.assertEqual('我', rst) + self.assertEqual("我", rst) self.assertEqual(str, type(rst)) # load gbk, result in str, in gbk encoding gbk = b'"\xb6\xd4\xd5\xbd\xc6\xbd\xcc\xa8\xb9\xd9\xb7\xbd\xd7\xee\xd0\xc2\xb0\xe6"' - self.assertEqual('对战平台官方最新版', k3utfjson.load(gbk, encoding="gbk")) + self.assertEqual("对战平台官方最新版", k3utfjson.load(gbk, encoding="gbk")) self.assertEqual(str, type(k3utfjson.load(gbk, encoding="gbk"))) # load any s = '"\xbb"' rst = k3utfjson.load(s) - self.assertEqual('\xbb', rst) + self.assertEqual("\xbb", rst) self.assertEqual(str, type(rst)) def test_load_backslash_x_encoded(self): s = '"\x61"' - self.assertEqual('a', k3utfjson.load(s)) + self.assertEqual("a", k3utfjson.load(s)) s = '"\x61"' - self.assertEqual('a', k3utfjson.load(s)) + self.assertEqual("a", k3utfjson.load(s)) s = b'"\xe6\x88\x91"' - self.assertEqual('我', k3utfjson.load(s)) + self.assertEqual("我", k3utfjson.load(s)) self.assertRaises(json.JSONDecodeError, k3utfjson.load, '"\\"') self.assertRaises(json.JSONDecodeError, k3utfjson.load, '"\\x"') self.assertRaises(json.JSONDecodeError, k3utfjson.load, '"\\x6"') def test_load_decode(self): - self.assertEqual('我', k3utfjson.load('"我"')) - self.assertEqual(u'我', k3utfjson.load('"我"', encoding='utf-8')) - self.assertEqual(str, type(k3utfjson.load('"我"', encoding='utf-8'))) - - self.assertEqual({'a': u"我"}, k3utfjson.load('{"a": "\\u6211"}')) - self.assertEqual({'a': u"我"}, k3utfjson.load('{"a": "我"}', encoding='utf-8')) - self.assertEqual({'a': "我"}, k3utfjson.load('{"a": "我"}')) - self.assertEqual({'a': "我"}, k3utfjson.load('{"a": "我"}')) + self.assertEqual("我", k3utfjson.load('"我"')) + self.assertEqual("我", k3utfjson.load('"我"', encoding="utf-8")) + self.assertEqual(str, type(k3utfjson.load('"我"', encoding="utf-8"))) + + self.assertEqual({"a": "我"}, k3utfjson.load('{"a": "\\u6211"}')) + self.assertEqual({"a": "我"}, k3utfjson.load('{"a": "我"}', encoding="utf-8")) + self.assertEqual({"a": "我"}, k3utfjson.load('{"a": "我"}')) + self.assertEqual({"a": "我"}, k3utfjson.load('{"a": "我"}')) self.assertEqual(["我"], k3utfjson.load('["我"]')) def test_dump(self): - self.assertEqual('null', k3utfjson.dump(None)) - self.assertEqual('{}', k3utfjson.dump({})) + self.assertEqual("null", k3utfjson.dump(None)) + self.assertEqual("{}", k3utfjson.dump({})) self.assertRaises(TypeError, k3utfjson.dump, b"\xe6\x88\x91", encoding=None) self.assertRaises(TypeError, k3utfjson.dump, {b"\xe6\x88\x91": 1}, encoding=None) @@ -84,35 +83,41 @@ def test_dump(self): self.assertRaises(TypeError, k3utfjson.dump, [b"\xe6\x88\x91"], encoding=None) self.assertRaises(TypeError, k3utfjson.dump, [(b"\xe6\x88\x91",)], encoding=None) - self.assertEqual('"\\u6211"', k3utfjson.dump(u'我', encoding=None)) - self.assertEqual("\"" + b'\xb6\xd4'.decode('gbk') + "\"", k3utfjson.dump(u'对', encoding='gbk')) - self.assertEqual("\"" + b"\xe6\x88\x91".decode("utf-8") + "\"", k3utfjson.dump('我', encoding='utf-8')) + self.assertEqual('"\\u6211"', k3utfjson.dump("我", encoding=None)) + self.assertEqual('"' + b"\xb6\xd4".decode("gbk") + '"', k3utfjson.dump("对", encoding="gbk")) + self.assertEqual('"' + b"\xe6\x88\x91".decode("utf-8") + '"', k3utfjson.dump("我", encoding="utf-8")) - self.assertEqual("\"" + b"\xe6\x88\x91".decode("utf-8") + "\"", k3utfjson.dump(u'我')) - self.assertEqual("\"" + b"\xe6\x88\x91".decode("utf-8") + "\"", k3utfjson.dump('我')) + self.assertEqual('"' + b"\xe6\x88\x91".decode("utf-8") + '"', k3utfjson.dump("我")) + self.assertEqual('"' + b"\xe6\x88\x91".decode("utf-8") + '"', k3utfjson.dump("我")) # by default unicode are encoded - self.assertEqual("{\"" + b"\xe6\x88\x91".decode("utf-8") + "\": \"" + b"\xe6\x88\x91".decode("utf-8") + "\"}" - , k3utfjson.dump({"我": "我"})) - self.assertEqual("{\"" + b"\xe6\x88\x91".decode("utf-8") + "\": \"" + b"\xe6\x88\x91".decode("utf-8") + "\"}" - , k3utfjson.dump({"我": u"我"})) - self.assertEqual("{\"" + b"\xe6\x88\x91".decode("utf-8") + "\": \"" + b"\xe6\x88\x91".decode("utf-8") + "\"}" - , k3utfjson.dump({u"我": "我"})) - self.assertEqual("{\"" + b"\xe6\x88\x91".decode("utf-8") + "\": \"" + b"\xe6\x88\x91".decode("utf-8") + "\"}" - , k3utfjson.dump({u"我": u"我"})) - self.assertEqual("[\""+b"\xe6\x88\x91".decode("utf-8") + "\"]", - k3utfjson.dump((u"我",))) + self.assertEqual( + '{"' + b"\xe6\x88\x91".decode("utf-8") + '": "' + b"\xe6\x88\x91".decode("utf-8") + '"}', + k3utfjson.dump({"我": "我"}), + ) + self.assertEqual( + '{"' + b"\xe6\x88\x91".decode("utf-8") + '": "' + b"\xe6\x88\x91".decode("utf-8") + '"}', + k3utfjson.dump({"我": "我"}), + ) + self.assertEqual( + '{"' + b"\xe6\x88\x91".decode("utf-8") + '": "' + b"\xe6\x88\x91".decode("utf-8") + '"}', + k3utfjson.dump({"我": "我"}), + ) + self.assertEqual( + '{"' + b"\xe6\x88\x91".decode("utf-8") + '": "' + b"\xe6\x88\x91".decode("utf-8") + '"}', + k3utfjson.dump({"我": "我"}), + ) + self.assertEqual('["' + b"\xe6\x88\x91".decode("utf-8") + '"]', k3utfjson.dump(("我",))) - self.assertEqual('{"\\u6211": "\\u6211"}', k3utfjson.dump({u"我": u"我"}, encoding=None)) + self.assertEqual('{"\\u6211": "\\u6211"}', k3utfjson.dump({"我": "我"}, encoding=None)) self.assertEqual('"\\""', k3utfjson.dump('"')) # encoded chars and unicode chars in one string - self.assertEqual('/aaa\xe7\x89\x88\xe6\x9c\xac/jfkdsl\x01', - k3utfjson.load('"\/aaa\xe7\x89\x88\xe6\x9c\xac\/jfkdsl\\u0001"')) - self.assertEqual( - '{\n "我": "我"\n}', k3utfjson.dump({"我": "我"}, indent=2)) - self.assertEqual( - '{\n "我": "我"\n}', k3utfjson.dump({"我": "我"}, indent=4)) + "/aaa\xe7\x89\x88\xe6\x9c\xac/jfkdsl\x01", k3utfjson.load('"\/aaa\xe7\x89\x88\xe6\x9c\xac\/jfkdsl\\u0001"') + ) + + self.assertEqual('{\n "我": "我"\n}', k3utfjson.dump({"我": "我"}, indent=2)) + self.assertEqual('{\n "我": "我"\n}', k3utfjson.dump({"我": "我"}, indent=4)) diff --git a/utfjson.py b/utfjson.py index 5c081f1..c4c057e 100644 --- a/utfjson.py +++ b/utfjson.py @@ -30,7 +30,7 @@ # str '\xe6\x88\x91' '"\\u00e6\\u0088\\u0091"' '"\xe6\x88\x91"' -def dump(obj, encoding='utf-8', indent=None): +def dump(obj, encoding="utf-8", indent=None): # - Using non-unicode with ensure_ascii=True results in unexpected output: # p3fjson.dump('我', ensure_ascii=True) --> '"\\u00e6\\u0088\\u0091"' # @@ -57,10 +57,8 @@ def dump(obj, encoding='utf-8', indent=None): def ensure_str(o): - if isinstance(o, bytes): - raise TypeError('string({o} {tp}) must be str' - ' if ensure_ascii is True'.format(o=o, tp=type(o))) + raise TypeError("string({o} {tp}) must be str if ensure_ascii is True".format(o=o, tp=type(o))) if isinstance(o, dict): for k, v in o.items(): From ed23189d63c73f5ecdff49c89f7023aa3a754bf0 Mon Sep 17 00:00:00 2001 From: Zhang Yanpo Date: Wed, 7 Jan 2026 23:29:37 +0800 Subject: [PATCH 2/2] fix: add k3ut test dependency to workflow --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 4575dad..5c5dd19 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -21,7 +21,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pytest + pip install pytest k3ut pip install -e . - name: Test with pytest run: |